Update memory-manager-concurrent

This commit is contained in:
Cola-Echo
2026-01-21 18:11:33 +08:00
commit f51c4ef6dc
418 changed files with 400794 additions and 0 deletions

1581
games/mario/3rd/backbone.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,481 @@
/**
* Backbone.Native
*
* For all details and documentation:
* http://github.com/inkling/backbone.native
*
* Copyright 2013 Inkling Systems, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* The purpose of this library is to allow Backbone to work without needing to load jQuery or Zepto.
* This file provides a basic jQuery-like implementation for Backbone, implementing the
* minimum functionality for Backbone to function. We assume that Backbone applications using
* this will not expect the standard jQuery API to work, and will instead use native JS functions.
*
* Keep in mind that due to the APIs in this, it will likely only work on recent browsers.
*
* Note:
* - Core Backbone only needs collections with single members, so that is all that has been
* supported in this library. It is expected that you will just use querySelectorAll instead.
* This will be most obvious if you make heavy use of 'view.$'.
* - Events delegated with selectors starting with '>' are not supported.
* - Due to 'currentTarget' being read-only on standard DOM events, we cannot make standard
* events behave identically to jQuery's events when delegation is used. The element matching
* the delegate selector is instead passed as the second argument to event handlers.
* - The '$.ajax' implementation is very simple and likely needs to be expanded to better support
* standard use-cases.
*
* Tested with Backbone v0.9.2 and 1.0.0.
*/
(function(){
"use strict";
// Regular expression to match an event name and/or a namespace.
var namespaceRE = /^([^.]+)?(?:\.([^.]+))?$/;
var matchesSelector = Element.prototype.matchesSelector || null;
if (!matchesSelector){
['webkit', 'moz', 'o', 'ms'].forEach(function(prefix){
var func = Element.prototype[prefix + 'MatchesSelector'];
if (func) matchesSelector = func;
});
}
// The element property to save the cache key on.
var cacheKeyProp = 'backboneNativeKey' + Math.random();
var id = 1;
var handlers = {};
var unusedKeys = [];
/**
* Get the event handlers for a given element, creating an empty set if one doesn't exist.
*
* To avoid constantly filling the handlers object with null values, we reuse old IDs that
* have been created and then cleared.
*
* @param {Element} el The element to get handlers for.
*
* @return {Array} An array of handlers.
*/
function handlersFor(el){
if (!el[cacheKeyProp]){
// Pick a new key, from the unused pool, or make a new one.
el[cacheKeyProp] = unusedKeys.length === 0 ? ++id : unusedKeys.pop();
}
var cacheKey = el[cacheKeyProp];
return handlers[cacheKey] || (handlers[cacheKey] = []);
}
/**
* Clear the event handlers for a given element.
*
* @param {Element} el The element to clear.
*/
function clearHandlers(el){
var cacheKey = el[cacheKeyProp];
if (handlers[cacheKey]){
handlers[cacheKey] = null;
el[cacheKeyProp] = null;
unusedKeys.push(cacheKey);
}
}
/**
* Add event handlers to an element.
*
* @param {Element} parentElement The element to bind event handlers to.
* @param {string} eventName The event to bind, e.g. 'click'.
* @param {string} selector (Optional) The selector to match when an event propagates up.
* @param {function(Event, Element)} callback The function to call when the event is fired.
*/
function on(parentElement, eventName, selector, callback){
// Adjust arguments if selector was not provided.
if (typeof selector === 'function'){
callback = selector;
selector = null;
}
var parts = namespaceRE.exec(eventName);
eventName = parts[1] || null;
var namespace = parts[2] || null;
if (!eventName) return;
var handler = callback;
var originalCallback = callback;
if (selector){
// Event delegation handler to match a selector for child element events.
handler = function(event){
for (var el = event.target; el && el !== parentElement; el = el.parentElement){
if (matchesSelector.call(el, selector)){
// jQuery does not include the second argument, but we have included it
// for simplicity because 'this' will likely be bound to the view inside
// the callback, and as noted above, we cannot override 'currentTarget'.
var result = originalCallback.call(el, event, el);
if (result === false){
event.stopPropagation();
event.preventDefault();
}
return result;
}
}
};
} else {
// Standard event handler bound directly to the element.
handler = function(event){
var result = originalCallback.call(parentElement, event, parentElement);
if (result === false){
event.stopPropagation();
event.preventDefault();
}
return result;
};
}
parentElement.addEventListener(eventName, handler, false);
// Save event handler metadata so that the handler can be unbound later.
handlersFor(parentElement).push({
eventName: eventName,
callback: callback,
handler: handler,
namespace: namespace,
selector: selector
});
}
/**
* Remove an event handler from an element.
*
* @param {Element} parentElement The element to unbind event handlers from.
* @param {string} eventName (Optional) The event to unbind, e.g. 'click'.
* @param {string} selector (Optional) The selector to unbind.
* @param {function(Event, Element)} callback (Optional) The function to unbind.
*/
function off(parentElement, eventName, selector, callback){
if (typeof selector === 'function'){
callback = selector;
selector = null;
}
var parts = namespaceRE.exec(eventName || '');
eventName = parts[1];
var namespace = parts[2];
var handlers = handlersFor(parentElement) || [];
if (!eventName && !namespace && !selector && !callback){
// Fastpath to remove all handlers.
handlers.forEach(function(item){
parentElement.removeEventListener(item.eventName, item.handler, false);
});
clearHandlers(parentElement);
} else {
var matchedHandlers = handlers.filter(function(item){
return ((!namespace || item.namespace === namespace) &&
(!eventName || item.eventName === eventName) &&
(!callback || item.callback === callback) &&
(!selector || item.selector === selector));
});
matchedHandlers.forEach(function(item){
parentElement.removeEventListener(item.eventName, item.handler, false);
handlers.splice(handlers.indexOf(item), 1);
});
if (handlers.length === 0) clearHandlers(parentElement);
}
}
/**
* Construct a new jQuery-style element representation.
*
* @param {string|Element|Window} element There are several different possible values for this
* argument:
* - {string} A snippet of HTML, if it starts with a '<', or a selector to find.
* - {Element} An existing element to wrap.
* - {Window} The window object to wrap.
* @param {Element} context The context to search within, if a selector was given.
* Defaults to document.
*/
function $(element, context){
context = context || document;
// Call as a constructor if it was used as a function.
if (!(this instanceof $)) return new $(element, context);
if (!element){
this.length = 0;
} else if (typeof element === 'string'){
if (/^\s*</.test(element)){
// Parse arbitrary HTML into an element.
var div = document.createElement('div');
div.innerHTML = element;
this[0] = div.firstChild;
div.removeChild(div.firstChild);
this.length = 1;
} else {
element = context.querySelector(element);
// Length must be 0 if no elements found.
if (element !== null){
this[0] = element;
this.length = 1;
} else {
this.length = 0;
}
}
} else {
// This handles both the 'Element' and 'Window' case, as both support
// event binding via 'addEventListener'.
this[0] = element;
this.length = 1;
}
}
$.prototype = {
/**
* The following methods are used by Backbone, but only in code-paths for IE 6/7 support.
* Since none of this will work for old IE anyway, they are not implemented, and
* instead left for documentation purposes.
*
* Used in Backbone.History.prototype.start.
*/
hide: null,
appendTo: null,
/**
* Find is not supported to encourage the use of querySelector(All) as an alternative.
*
* e.g.
* Instead of 'this.$(sel)', use 'this.el.querySelectorAll(sel)'.
*
* Used in Backbone.View.prototype.$, but not actually called internally.
*/
find: null,
/**
* Add attributes to the element.
*
* Used in Backbone.View.prototype.make.
*
* @param {Object} attributes A set of attributes to apply to the element.
*
* @return {$} This instance.
*/
attr: function(attrs){
Object.keys(attrs).forEach(function(attr){
switch (attr){
case 'html':
this[0].innerHTML = attrs[attr];
break;
case 'text':
this[0].textContent = attrs[attr];
break;
case 'class':
this[0].className = attrs[attr];
break;
default:
this[0].setAttribute(attr, attrs[attr]);
break;
}
}, this);
return this;
},
/**
* Set the HTML content of the element. Backbone does not use the no-argument version
* to read innerHTML, so that has not been implemented.
*
* Used in Backbone.View.prototype.make.
*
* @param {string} html The HTML to set as the element content.
*
* @return {$} This instance.
*/
html: function(html){
this[0].innerHTML = html;
return this;
},
/**
* Remove an element from the DOM and remove all event handlers bound to it and
* its child elements.
*
* Used in Backbone.View.prototype.remove.
*
* @return {$} This instance.
*/
remove: function(){
var el = this[0];
if (el.parentElement) el.parentElement.removeChild(el);
// Unbind all event handlers on the element and children.
(function removeChildEvents(element){
off(element);
for (var i = 0, len = element.childNodes.length; i < len; i++){
if (element.childNodes[i].nodeType !== Node.TEXT_NODE){
removeChildEvents(element.childNodes[i]);
}
}
})(el);
return this;
},
/**
* Bind an event handler to this element.
*
* @param {string} eventName The event to bind, e.g. 'click'.
* @param {string} selector (Optional) The selector to match when an event propagates up.
* @param {function(Event, Element)} callback The function to call when the event is fired.
*/
on: function(eventName, selector, callback){
on(this[0], eventName, selector, callback);
return this;
},
/**
* Unbind an event handler to this element.
*
* @param {string} eventName (Optional) The event to unbind, e.g. 'click'.
* @param {string} selector (Optional) The selector to unbind.
* @param {function(Event, Element)} callback (Optional) The function to unbind.
*/
off: function(eventName, selector, callback){
off(this[0], eventName, selector, callback);
return this;
},
// Backbone v0.9.2 support.
bind: function(eventName, callback){
return this.on(eventName, callback);
},
unbind: function(eventName, callback){
return this.off(eventName, callback);
},
delegate: function(selector, eventName, callback){
return this.on(eventName, selector, callback);
},
undelegate: function(selector, eventName, callback){
return this.off(eventName, selector, callback);
}
};
/**
* Send an AJAX request.
*
* @param {Object} options The options to use for the connection:
* - {string} url The URL to connect to.
* - {string} type The type of request, e.g. 'GET', or 'POST'.
* - {string} dataType The type of data expected, 'json'.
* - {string} contentType The content-type of the data.
* - {string|object} data The content to send.
* - {function(XMLHttpRequest)} beforeSend A callback to call before sending.
* - {boolean} processData True if 'data' should be converted
* to a query string from an object.
* - {function({string|object}, {string}, {XMLHttpRequest})} success The success callback.
* - {function({XMLHttpRequest})} error The error callback.
*/
$.ajax = function(options){
options = options || {};
var type = options.type || 'GET';
var url = options.url;
var processData = options.processData === undefined ? true : !!options.processData;
var contentType = options.contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
// Process the data for sending.
var data = options.data;
if (processData && typeof data === 'object'){
var params = Object.keys(data).map(function(prop){
return encodeURIComponent(prop) + '=' + encodeURIComponent(data[prop]);
});
data = params.join('&');
}
// Data for GET and HEAD goes in the URL.
if (data && (type === 'GET' || type === 'HEAD')){
url += (url.indexOf('?') === -1 ? '?' : '&') + data;
data = undefined;
}
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
xhr.setRequestHeader('Content-Type', contentType);
if (options.beforeSend) options.beforeSend(xhr);
xhr.onload = function(){
var error = false;
var content = xhr.responseText;
// Parse the JSON before calling success.
if (options.dataType === 'json'){
try {
content = JSON.parse(content);
} catch (e){
error = true
}
}
if (!error && (xhr.status >= 200 && xhr.status < 300)){
// The last two arguments only apply to v0.9.2.
if (options.success) options.success(content, xhr.statusText, xhr);
} else {
// This signature is inconsistent with v0.9.2, but is correct for 1.0.0.
if (options.error) options.error(xhr);
}
}.bind(this);
xhr.onerror = xhr.onabort = function(){
if (options.error) options.error(xhr);
};
xhr.send(data);
return xhr;
};
// Expose on/off for external use with having to instantiate a wrapper.
$.on = on;
$.off = off;
if (typeof exports !== 'undefined') {
module.exports = $;
} else {
var root = this;
var originalBackboneNative = root.Backbone ? root.Backbone.Native : null;
var original$ = root.$;
if (root.Backbone) root.Backbone.Native = $;
root.$ = $;
$.noConflict = function(deep){
root.$ = original$;
if (deep) root.Backbone.Native = originalBackboneNative;
return $;
};
if (root.Backbone){
if (root.Backbone.setDomLibrary){ // v0.9.2
root.Backbone.setDomLibrary($);
} else { // v1.0.0
root.Backbone.$ = $;
}
}
}
}).call(this);

358
games/mario/3rd/qtree.js Normal file
View File

@@ -0,0 +1,358 @@
/**
*
* simple-quadtree is a minimal quadtree implementation that supports simple put, get,
* remove and clear operations on objects having a x, y position and w, h dimension.
*
* Copyright (c) 2013 Antti Saarinen <antti.p.saarinen@gmail.com>
* https://github.com/asaarinen/qtree
*
*/
function QuadTree(x, y, w, h, options) {
if( typeof x != 'number' || isNaN(x) )
x = 0;
if( typeof y != 'number' || isNaN(y) )
y = 0;
if( typeof w != 'number' || isNaN(w) )
w = 10;
if( typeof h != 'number' || isNaN(h) )
h = 10;
var maxc = 25;
var leafratio = 0.5;
if( options ) {
if( typeof options.maxchildren == 'number' )
if( options.maxchildren > 0 )
maxc = options.maxchildren;
if( typeof options.leafratio == 'number' )
if( options.leafratio >= 0 )
leafratio = options.leafratio;
}
// validate an input object
function validate(obj) {
if( !obj )
return false;
if( typeof obj.x != 'number' ||
typeof obj.y != 'number' ||
typeof obj.w != 'number' ||
typeof obj.h != 'number' )
return false;
if( isNaN(obj.x) || isNaN(obj.y) ||
isNaN(obj.w) || isNaN(obj.h) )
return false;
return true;
}
// test for deep equality for x,y,w,h
function isequal(o1, o2) {
if( o1.x == o2.x &&
o1.y == o2.y &&
o1.w == o2.w &&
o1.h == o2.h )
return true;
return false;
}
// create a new quadtree node
function createnode(x, y, w, h) {
return {
x: x,
y: y,
w: w,
h: h,
c: [],
l: [],
n: []
}
}
// root node used by this quadtree
var root = createnode(x, y, w, h);
// calculate distance between two points
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
}
// calculate distance between a point and a line (segment)
function distancePL(x, y, x1, y1, dx1, dy1, len1 ) {
if( !len1 ) // in case length is not provided, assume a line
len1 = -1;
// x = x1 + s * dx1 + t * dy1
// y = y1 + s * dy1 - t * dx1
// x * dy1 - y * dx1 = x1 * dy1 - y1 * dx1 +
// t * ( dy1 * dy1 + dx1 * dx1 )
var t = dx1 * dx1 + dy1 * dy1;
if( t == 0 )
return null;
else {
t = ( x * dy1 - y * dx1 - x1 * dy1 + y1 * dx1 ) / t;
if( Math.abs(dx1) > Math.abs(dy1) )
var s = ( x - x1 - t * dy1 ) / dx1;
else
var s = ( y - y1 + t * dx1 ) / dy1;
if( ( s >= 0 && s <= len1 ) || len1 < 0 )
return {
s: s,
t: t,
x: x1 + s * dx1,
y: y1 + s * dy1,
dist: Math.abs(t)
};
else if( s < 0 ) {
var dist = distance(x, y, x1, y1);
return {
s: s,
dist: dist
};
} else {
var dist = distance(x, y,
x1 + len1*dx1,
y1 + len1*dy1);
return {
s: s,
dist: dist
};
}
}
}
// does a line and a rectangle overlap ?
function overlap_line(o1, o2, buf) {
if( !o1 || !o2 )
return true;
var dist = distancePL(o2.x + 0.5 * o2.w,
o2.y + 0.5 * o2.h,
o1.x, o1.y, o1.dx, o1.dy, o1.dist);
if( dist ) {
dist.dist -= buf;
if( dist.dist < 0 )
return true;
if( dist.dist * dist.dist <= o2.w * o2.w + o2.h * o2.h )
return true;
}
return false;
}
// do two rectangles overlap ?
function overlap_rect(o1, o2, buf) {
if( !o1 || !o2 )
return true;
if( o1.x + o1.w < o2.x - buf ||
o1.y + o1.h < o2.y - buf ||
o1.x - buf > o2.x + o2.w ||
o1.y - buf > o2.y + o2.h )
return false;
return true;
}
function isleaf(node, obj) {
var leaf = false;
if( obj.w * obj.h > node.w * node.h * leafratio )
leaf = true;
if( obj.x < node.x ||
obj.y < node.y ||
obj.x + obj.w > node.x + node.w ||
obj.y + obj.h > node.y + node.h )
leaf = true;
var childnode = null;
for( var ni = 0; ni < node.n.length; ni++ )
if( overlap_rect(obj, node.n[ni], 0) ) {
if( childnode ) { // multiple hits
leaf = true;
break;
} else
childnode = node.n[ni];
}
return { leaf: leaf,
childnode: childnode };
}
// put an object to one of the child nodes of this node
function put_to_nodes(node, obj) {
var leaf = isleaf(node, obj);
if( leaf )
node.l.push(obj);
else if( leaf.childnode )
put(leaf.childnode, obj);
else
return;
}
// remove an object from this node
function remove(node, obj, attr) {
if( !validate(obj) )
return 0;
if( !attr )
attr = false;
else if( typeof attr != 'string' )
attr = 'id';
var count = 0;
for( var ci = 0; ci < node.c.length; ci++ )
if( ( attr && node.c[ci][attr] == obj[attr] ) ||
( !attr && isequal(node.c[ci], obj) ) ) {
count++;
node.c.splice(ci, 1);
ci--;
}
for( var ci = 0; ci < node.l.length; ci++ )
if( ( attr && node.l[ci][attr] == obj[attr] ) ||
( !attr && isequal(node.l[ci], obj) ) ) {
count++;
node.l.splice(ci, 1);
ci--;
}
var leaf = isleaf(node, obj);
if( !leaf.leaf && leaf.childnode )
return count + remove(leaf.childnode, obj, attr);
return count;
}
// put an object to this node
function put(node, obj, removeflag) {
if( !validate(obj) )
return;
if( node.n.length == 0 ) {
node.c.push(obj);
// subdivide
if( node.c.length > maxc ) {
var w2 = node.w / 2;
var h2 = node.h / 2;
node.n.push(createnode(node.x, node.y, w2, h2),
createnode(node.x + w2, node.y, w2, h2),
createnode(node.x, node.y + h2, w2, h2),
createnode(node.x + w2, node.y + h2, w2, h2));
for( var ci = 0; ci < node.c.length; ci++ )
put_to_nodes(node, node.c[ci]);
node.c = [];
}
} else
put_to_nodes(node, obj);
}
// iterate through all objects in this node matching given overlap
// function
function getter(overlapfun, node, obj, buf, strict, callbackOrArray) {
for( var li = 0; li < node.l.length; li++ )
if( !strict || overlapfun(obj, node.l[li], buf) )
if( typeof callbackOrArray == 'object' )
callbackOrArray.push(node.l[li]);
else if( !callbackOrArray(node.l[li]) )
return false;
for( var li = 0; li < node.c.length; li++ )
if( !strict || overlapfun(obj, node.c[li], buf) )
if( typeof callbackOrArray == 'object' )
callbackOrArray.push(node.c[li]);
else if( !callbackOrArray(node.c[li]) )
return false;
for( var ni = 0; ni < node.n.length; ni++ ) {
if( overlapfun(obj, node.n[ni], buf) ) {
if( typeof callbackOrArray == 'object' )
callbackOrArray.concat(getter(overlapfun, node.n[ni], obj, buf, strict, callbackOrArray));
else if( !getter(overlapfun, node.n[ni], obj, buf, strict, callbackOrArray) )
return false;
}
}
return true;
}
// iterate through all objects in this node matching the given rectangle
function get_rect(node, obj, buf, callbackOrArray) {
return getter(overlap_rect, node, obj, buf, true, callbackOrArray);
}
// iterate through all objects in this node matching the given
// line (segment)
function get_line(node, obj, buf, callbackOrArray) {
return getter(overlap_line, node, obj, buf, false, callbackOrArray);
}
// iterate through all objects in this node matching given
// geometry, either a rectangle or a line segment
function get(node, obj, buf, callbackOrArray) {
if( typeof buf == 'function' && typeof callbackOrArray == 'undefined' ) {
callbackOrArray = buf;
buf = 0;
}
if( typeof callbackOrArray == 'undefined' ) {
callbackOrArray = [];
buf = 0;
}
if( obj == null )
get_rect(node, obj, buf, callbackOrArray);
else if( typeof obj.x == 'number' &&
typeof obj.y == 'number' &&
!isNaN(obj.x) && !isNaN(obj.y) ) {
if( typeof obj.dx == 'number' &&
typeof obj.dy == 'number' &&
!isNaN(obj.dx) && !isNaN(obj.dy) )
get_line(node, obj, buf, callbackOrArray);
else if( typeof obj.w == 'number' &&
typeof obj.h == 'number' &&
!isNaN(obj.w) && !isNaN(obj.h) )
get_rect(node, obj, buf, callbackOrArray);
}
if( typeof callbackOrArray == "object" ) return callbackOrArray;
}
// return the object interface
return {
get: function(obj, buf, callbackOrArray) {
return get(root, obj, buf, callbackOrArray);
},
put: function(obj) {
put(root, obj);
},
remove: function(obj, attr) {
return remove(root, obj, attr);
},
clear: function() {
root = createnode(x, y, w, h);
},
stringify: function() {
var strobj = {
x: x, y: y, w: w, h: h,
maxc: maxc,
leafratio: leafratio,
root: root
};
try {
return JSON.stringify(strobj);
} catch(err) {
// could not stringify
// probably due to objects included in qtree being non-stringifiable
return null;
}
},
parse: function(str) {
if( typeof str == 'string' )
str = JSON.parse(str);
x = str.x;
y = str.y;
w = str.w;
h = str.h;
maxc = str.maxc;
leafratio = str.leafratio;
root = str.root;
}
};
}
// for use within node.js
if( typeof module != 'undefined' )
module.exports = QuadTree;

File diff suppressed because it is too large Load Diff

20
games/mario/LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 AmiliaApp
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.

12
games/mario/README.md Normal file
View File

@@ -0,0 +1,12 @@
Backbone Game Engine
====================
An elementary HTML5 Game Engine built on Backbone.
Documentation and examples can be found here:
http://martindrapeau.github.io/backbone-game-engine
* * *
Copyright (c) Martin Drapeau<br/>
Licensed under the MIT @license

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

View File

@@ -0,0 +1,45 @@
<!doctype html>
<html style="touch-action: none;">
<head>
<title>Mario - Backbone Game Engine</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="../favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="../apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=960, user-scalable=no"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<script src="../3rd/underscore.js" type="text/javascript"></script>
<script src="../3rd/backbone.native.js" type="text/javascript"></script>
<script src="../3rd/backbone.js" type="text/javascript"></script>
<script src="../src/adjust-viewport.js" type="text/javascript"></script>
<script src="../src/shapes.js" type="text/javascript"></script>
<script src="../src/core.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<style>
body {
margin: 0;
background-color: #000;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
</style>
</head>
<body>
<canvas id="foreground" width="960" height="700">
Your browser does not support canvas element.
</canvas>
</body>
</html>

107
games/mario/ball/main.js Normal file
View File

@@ -0,0 +1,107 @@
$(window).on("load", function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
// Bouncing ball.
Backbone.Ball = Backbone.Sprite.extend({
defaults: _.extend(Backbone.Sprite.prototype.defaults, {
x: 400,
y: 400,
radius: 20,
color: "red",
velocity: 400,
yVelocity: 400
}),
update: function(dt) {
if (!this.engine) return false;
var x = this.get("x"),
y = this.get("y"),
radius = this.get("radius"),
velocity = this.get("velocity") - radius,
yVelocity = this.get("yVelocity") - radius,
maxX = this.engine.canvas.width - radius,
maxY = this.engine.canvas.height - radius,
attrs = {oldX: x, oldY: y};
x += velocity * (dt/1000);
y += yVelocity * (dt/1000);
if (x <= 0) {
x = 0;
attrs.velocity = velocity * -1;
}
if (x >= maxX) {
x = maxX;
attrs.velocity = velocity * -1;
}
if (y <= 0) {
y = 0;
attrs.yVelocity = yVelocity * -1;
}
if (y >= maxY) {
y = maxY;
attrs.yVelocity = yVelocity * -1;
}
attrs.x = x;
attrs.y = y;
this.set(attrs);
return true;
},
draw: function(context) {
var x = this.get("x"),
y = this.get("y"),
oldX = this.get("oldX"),
oldY = this.get("oldY"),
radius = this.get("radius"),
color = this.get("color");
if (oldX || oldY)
context.clearRect(oldX, oldY, radius, radius);
drawCircle(context, x+radius/2, y+radius/2, radius, color);
return this;
}
});
var canvas = document.getElementById("foreground");
var debugPanel = new Backbone.DebugPanel();
var ball = new Backbone.Ball({
x: 100,
y: 100,
color: "blue"
});
var engine = new Backbone.Engine({
clearOnDraw: true
}, {
canvas: canvas,
debugPanel: debugPanel
});
engine.add([
ball,
debugPanel
]);
// Expose things as globals - easier to debug
_.extend(window, {
canvas: canvas,
engine: engine
});
adjustViewport(canvas);
});

BIN
games/mario/docs/ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

7
games/mario/docs/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
games/mario/docs/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

66
games/mario/docs/docs.css Normal file
View File

@@ -0,0 +1,66 @@
a {
color: #4242FF;
}
.navbar-inverse {
border-radius: 0;
background-color: #4242FF;
border-color: #4242FF;
}
.navbar-inverse .navbar-collapse,
.navbar-inverse .navbar-form {
border-color: #4242FF;
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav>li>a {
color: #F0F0F0;
}
a.navbar-brand {
padding-left: 50px;
}
a.navbar-brand img {
display: block;
position: absolute;
height: 32px;
margin: -8px 0 0 -42px;
}
footer.navbar-default {border-radius: 0; margin-bottom: 0;}
.bs-sidebar .nav > .active > ul {
display: block;
margin-bottom: 4px;
}
.bs-sidebar .nav > li > a {
padding: 2px 4px;
}
.bs-sidebar .nav > li.active {
background-color: #428bca;
border-radius: 0;
}
.bs-sidebar .nav > li.active > a {
color: white;
}
.bs-sidebar .nav > li.active > a:hover {
background-color: #428bca;
}
li.github-icon {
padding-left: 10px;
}
li.github-icon a {
display: inline-block;
padding-left: 32px;
color: white;
}
li.github-icon a img {
display: block;
position: absolute;
width: 32px;
margin: -7px 0 0 -32px;
opacity: 0.9;
}
li.github-icon a:hover img {
opacity: 1.0;
}
.bs-sidebar .nav > li.active,
.bs-sidebar .nav > li.active > a:hover {
background-color: #4242FF;
}

BIN
games/mario/docs/frog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
games/mario/docs/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
games/mario/docs/input.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

4
games/mario/docs/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
games/mario/docs/mario.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

113
games/mario/examples.html Normal file
View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Examples - Backbone Game Engine</title>
<meta name="description" content="Games and samples created with Backbone Game Engine.">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<script src="docs/jquery.min.js" type="text/javascript"></script>
<script src="docs/bootstrap.min.js" type="text/javascript"></script>
<link href="docs/bootstrap.min.css" rel="stylesheet" type="text/css" charset="utf-8">
<link href="docs/docs.css" rel="stylesheet" type="text/css" charset="utf-8">
</head>
<body data-spy="scroll" data-target="#sidebar" data-offset="100">
<header class="navbar navbar-inverse" role="banner">
<div class="navbar-header">
<button type="button" class="navbar-toggle pull-left" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="index.html" title="Elementary HTML5 Canvas Game Engine based on Backbone."><img src="apple_touch_icon.png" /> Backbone Game Engine</a>
</div>
<div id="navbar-collapse" class="collapse navbar-collapse" role="navigation">
<ul class="nav navbar-nav">
<li><a href="index.html">Documentation</a></li>
<li><a href="examples.html">Examples</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="github-icon"><a href="https://github.com/martindrapeau/backbone-game-engine" title="Fork me on Github"><img src="docs/github.png" />Github</a></li>
</ul>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Examples</h1>
<p>
Some examples built with Backbone Game Engine. Play and modify at will.
</p>
</div>
</div>
<div class="row">
<div class="col-md-4 col-xs-6">
<h4>Super Mario Bros, level 1-1</h4>
<a href="super-mario-bros/index.html" target="_blank">
<img src="docs/super-mario-bros-level-1-1.png" class="img-responsive" alt="Super Mario Bros, level 1-1" title="Super Mario Bros, level 1-1" />
</a>
</div>
<div class="col-md-4 col-xs-6">
<h4>Mariocraft</h4>
<a href="http://www.mariocraft.club" target="_blank">
<img src="docs/mariocraft.png" class="img-responsive" alt="Mariocraft" title="Kids can build and show-off their own level." />
</a>
</div>
<div class="col-md-4 col-xs-6">
<h4>Ludo's Quest <small><a href="https://itunes.apple.com/ca/app/ludos-quest/id1047863228" target="_blank">(available on the App Store)</a></small></h4>
<a href="http://www.ludosquest.com" target="_blank">
<img src="docs/ludosquest.png" class="img-responsive" alt="Ludo's Quest" title="HTML5/iOS/Android side-scrolling platformer set in medieval times. Rated for children." />
</a>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-4 col-xs-6">
<h4>Bouncing ball</h4>
<a href="ball/index.html" target="_blank">
<img src="docs/ball.png" class="img-responsive" alt="Bouncing Ball" title="Bouncing Ball" />
</a>
</div>
<div class="col-md-4 col-xs-6">
<h4>Mario in an empty world</h4>
<a href="mario/index.html" target="_blank">
<img src="docs/mario.png" class="img-responsive" alt="Mario in an empty world" title="Mario in an empty world" />
</a>
</div>
<div class="col-md-4 col-xs-6">
<h4>Hoppy frog</h4>
<a href="frog/index.html" target="_blank">
<img src="docs/frog.png" class="img-responsive" alt="Hoppy frog" title="A frog that can only hop" />
</a>
</div>
</div>
</div>
<br/>
<br/>
<footer class="navbar navbar-default">
<p class="navbar-text navbar-left">
&copy; 2014-2015 <a href="http://martindrapeau.tumblr.com/">Martin Drapeau.</a>
<a href="https://github.com/martindrapeau/backbone-game-engine/blob/gh-pages/LICENSE">Licensed under MIT.</a>
</p>
<p class="navbar-text navbar-right">Written in Montréal, Canada.</p>
<p class="navbar-text navbar-right">&nbsp;</p>
</footer>
</body>
</html>

BIN
games/mario/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

145
games/mario/frog/frog.js Normal file
View File

@@ -0,0 +1,145 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var velocity = 200,
acceleration = 400,
jumpVelocity = 550,
jumpDeceleration = 1400,
jumpHoldDeceleration = 900,
fallVelocity = 600,
fallAcceleration = 1200;
Backbone.Frog = Backbone.Hero.extend({
defaults: _.extend({}, Backbone.Hero.prototype.defaults, {
name: "frog",
type: "character",
width: 50,
height: 60,
spriteSheet: "frog",
state: "idle-right",
velocity: 0,
acceleration: 0,
yVelocity: 0,
yAcceleration: 0
}),
animations: {
"idle-right": {
sequences: [1],
velocity: 0,
acceleration: 0,
yVelocity: 0,
yAcceleration: 0,
scaleX: 1,
scaleY: 1
},
"idle-left": {
sequences: [1],
velocity: 0,
acceleration: 0,
yVelocity: 0,
yAcceleration: 0,
scaleX: -1,
scaleY: 1
},
"jump-right": {
sequences: [2],
velocity: velocity,
acceleration: acceleration,
yStartVelocity: -jumpVelocity,
yEndVelocity: fallVelocity,
yAscentAcceleration: jumpDeceleration,
yHoldAscentAcceleration: jumpHoldDeceleration,
yDescentAcceleration: fallAcceleration,
scaleX: 1,
scaleY: 1
},
"jump-left": {
sequences: [2],
velocity: -velocity,
acceleration: acceleration,
yStartVelocity: -jumpVelocity,
yEndVelocity: fallVelocity,
yAscentAcceleration: jumpDeceleration,
yHoldAscentAcceleration: jumpHoldDeceleration,
yDescentAcceleration: fallAcceleration,
scaleX: -1,
scaleY: 1
},
"dead-left": _.extend({}, Backbone.Hero.prototype.animations["dead-left"], {sequences: [0]}),
"dead-right": _.extend({}, Backbone.Hero.prototype.animations["dead-right"], {sequences: [0]})
},
dirToggled: function(dirIntent) {
if (this.ignoreInput()) return this;
if (dirIntent != "left" && dirIntent != "right")
throw "Invalid or missing dirIntent. Must be left or right."
var cur = this.getStateInfo(),
opoIntent = dirIntent == "right" ? "left" : "right",
dirPressed = this.input ? this.input[dirIntent+"Pressed"]() : false,
opoPressed = this.input ? this.input[opoIntent+"Pressed"]() : false,
attrs = {};
if (dirPressed && cur.mov != "jump") {
attrs.state = this.buildState(cur.mov, dirIntent);
} else {
if (opoPressed) this.dirToggled(opoIntent);
}
if (!_.isEmpty(attrs)) this.set(attrs);
return this;
},
// Jump
buttonAToggled: function() {
if (this.ignoreInput()) return this;
var state = this.get("state"),
cur = this.getStateInfo(),
attrs = {};
if (this.input && this.input.buttonAPressed() && cur.mov != "jump") {
attrs.state = this.buildState("jump", cur.dir);
attrs.nextState = this.buildState("idle", cur.dir);
var jumpAnimation = this.getAnimation(attrs.state);
attrs.velocity = jumpAnimation.velocity;
attrs.yVelocity = jumpAnimation.yStartVelocity;
jumpAnimation.minY = this.get("y") - 200;
}
if (!_.isEmpty(attrs)) this.set(attrs);
return this;
},
// No action
buttonBToggled: function() {
return this;
},
onUpdate: function(dt) {
var cur = this.getStateInfo(),
velocity = this.get("velocity"),
attrs = {};
// Upon landing...
if (cur.mov == "idle" && velocity != 0) {
// No momentum
attrs.velocity = 0;
// Turn around
if (cur.dir == "left" && this.input && this.input.rightPressed() ||
cur.dir == "right" && this.input && this.input.leftPressed())
attrs.state = this.buildState(cur.mov, cur.opo);
}
if (!_.isEmpty(attrs)) this.set(attrs);
return true;
}
});
}).call(this);

BIN
games/mario/frog/frog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
games/mario/frog/icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,61 @@
<!doctype html>
<html style="touch-action: none;" nomanifest="offline.appcache">
<head>
<title>Frog - Backbone Game Engine</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="../favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="../apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=960, user-scalable=no"/>
<meta name="mobileoptimized" content="0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<script src="../3rd/qtree.js" type="text/javascript"></script>
<script src="../3rd/underscore.js" type="text/javascript"></script>
<script src="../3rd/backbone.native.js" type="text/javascript"></script>
<script src="../3rd/backbone.js" type="text/javascript"></script>
<script src="../src/adjust-viewport.js" type="text/javascript"></script>
<script src="../src/shapes.js" type="text/javascript"></script>
<script src="../src/core.js" type="text/javascript"></script>
<script src="../src/input.js" type="text/javascript"></script>
<script src="../src/character.js" type="text/javascript"></script>
<script src="../src/hero.js" type="text/javascript"></script>
<script src="../src/world.js" type="text/javascript"></script>
<script src="../src/local-storage.js" type="text/javascript"></script>
<script src="../src/camera.js" type="text/javascript"></script>
<script src="../src/editor.js" type="text/javascript"></script>
<script src="frog.js" type="text/javascript"></script>
<script src="tiles.js" type="text/javascript"></script>
<script src="level.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<style>
body {
margin: 0;
background-color: #000;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
</style>
</head>
<body>
<img id="frog" src="frog.png" style="display:none;" />
<img id="tiles" src="super-mario-tiles-2x.png" style="display:none;" />
<img id="icons" src="icons.png" style="display:none;" />
<canvas id="foreground" width="960" height="700">
Your browser does not support canvas element.
</canvas>
</body>
</html>

File diff suppressed because one or more lines are too long

237
games/mario/frog/main.js Normal file
View File

@@ -0,0 +1,237 @@
$(window).on("load", function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var canvas = document.getElementById("foreground"),
context = canvas.getContext("2d");
adjustViewport(canvas);
var spriteNames = [
"land1", "land2", "land3", "land4", "land5", "land6",
"mush1", "mush2", "mush3", "mush4", "mush5", "mush6",
"water1", "cloud1", "cloud2", "cloud3", "cloud-happy1", "cloud-happy2", "cloud-happy3",
"cloud-small", "ground", "land8", "ground2", "block", "land7", "block2",
"cloud-platform1", "cloud-platform2", "cloud-platform3", "cloud-platform4", "cloud-platform5", "cloud-platform6",
"water2", "cloud4", "cloud5", "cloud6", "cloud-happy4", "cloud-happy5", "cloud-happy6",
"frog", "platform"
];
Backbone.Controller = Backbone.Model.extend({
initialize: function(attributes, options) {
options || (options = {});
var controller = this;
_.bindAll(this, "onChangeState", "toggleState", "saveWorld", "loadWorld");
// Create our sprite sheets and attach them to existing sprite classes
this.spriteSheets = new Backbone.SpriteSheetCollection([{
id: "frog",
img: "#frog",
tileWidth: 50,
tileHeight: 65,
tileColumns: 3,
tileRows: 1
}, {
id: "tiles",
img: "#tiles",
tileWidth: 32,
tileHeight: 32,
tileColumns: 29,
tileRows: 28
}]).attachToSpriteClasses();
// Create the debug panel
this.debugPanel = new Backbone.DebugPanel();
// User input (turn off touchpad to start)
this.input = new Backbone.Input({
drawTouchpad: true
});
// Camera
this.camera = new Backbone.Camera();
// Our world
this.world = new Backbone.World(
_.extend({viewportBottom: 156}, window._world), {
input: this.input,
camera: this.camera
});
// Message
this.message = new Backbone.Message({
x: 480, y: 20
});
// Buttons
this.toggleButton = new Backbone.Button({
x: 4, y: 4, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 0, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.toggleButton.on("tap", this.toggleState, this);
this.saveButton = new Backbone.Button({
x: 904, y: 548, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 96, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.saveButton.on("tap", this.saveWorld, this);
this.restartButton = new Backbone.Button({
x: 904, y: 608, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 128, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.restartButton.on("tap", this.restartWorld, this);
this.downloadButton = new Backbone.Button({
x: 888, y: 10, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 64, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.downloadButton.on("tap", this.downloadNewVersion, this);
// The game engine
this.engine = new Backbone.Engine({}, {
canvas: canvas,
debugPanel: this.debugPanel
});
this.engine.add(_.compact([
this.world,
this.camera,
this.toggleButton,
this.message,
this.debugPanel
]));
// The sprite picker and editor
this.editor = new Backbone.WorldEditor({
spriteNames: spriteNames,
width: 34*20+4
}, {
world: this.world
});
// Controls
$(document).on("keypress.Controller", function(e) {
if (e.keyCode == 66 || e.keyCode == 98)
controller.engine.toggle(); // b to break the animation
else if (e.keyCode == 80 || e.keyCode == 112)
controller.toggleState(); // p to pause and pause
});
this.listenTo(this.world, "change:state", this.onChangeState);
this.onChangeState();
// If we have Application Cache active, load the latest world
this.loadWorld();
},
toggleState: function(e) {
var state = this.world.get("state");
this.world.set("state", state == "edit" ? "play" : "edit");
if (!this.engine.isRunning()) this.engine.start();
},
onChangeState: function() {
var state = this.world.get("state");
if (state == "edit") {
// Edit
context.clearRect(0, 0, canvas.width, canvas.height);
this.engine.remove(this.input);
this.engine.add(this.editor);
this.engine.add([this.saveButton, this.restartButton]);
this.toggleButton.set({imgX: 32});
} else {
// Play
context.clearRect(0, 0, canvas.width, canvas.height);
this.engine.remove(this.editor);
this.engine.remove([this.saveButton, this.restartButton]);
this.engine.add(this.input);
this.toggleButton.set({imgX: 0});
}
},
// Save our world to the server when changed. Debounce by 5 seconds
// and push back saving upon activity
saveWorld: function() {
var controller = this,
world = this.world
message = this.message;
message.show("Saving...");
world.save(null, {
success: function() {
setTimeout(function() {
message.hide();
}, 1000);
},
error: function(xhr, textStatus, errorThrown ) {
message.show("Error: " + errorThrown);
}
});
return this;
},
// Load our world from the server.
loadWorld: function() {
var controller = this,
world = this.world,
message = this.message;
message.show("Loading...");
world.fetch({
success: function() {
world.spawnSprites();
message.hide();
},
error: function(xhr, textStatus, errorThrown ) {
message.show('Error: ' + errorThrown);
setTimeout(function() {
message.hide();
}, 2000);
}
});
return this;
},
restartWorld: function() {
var controller = this,
world = this.world,
message = this.message;
message.show("Restarting...");
localStorage.removeItem(world.id);
world.set(window._world).spawnSprites();
setTimeout(function() {
message.hide();
}, 2000);
return this;
},
downloadNewVersion: function() {
window.applicationCache.swapCache();
this.message.show("Please wait...");
window.location.reload();
}
});
var controller = new Backbone.Controller();
// When a newer version is available, load it and inform
// the user it can be downloaded.
if (window.applicationCache !== undefined)
window.applicationCache.addEventListener('updateready', function() {
controller.engine.add(controller.downloadButton);
});
// Expose things as globals - easier to debug
_.extend(window, {
canvas: canvas,
context: context,
controller: controller
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

164
games/mario/frog/tiles.js Normal file
View File

@@ -0,0 +1,164 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
Backbone.Tile = Backbone.Sprite.extend({
defaults: {
type: "tile",
width: 32,
height: 32,
spriteSheet: "tiles",
state: "idle",
static: true,
persist: true
},
initialize: function(attributes, options) {
options || (options = {});
this.world = options.world;
this.lastSequenceChangeTime = 0;
}
});
function extendSprite(cls, name, attributes, animations) {
var newCls = _.classify(name);
Backbone[newCls] = Backbone[cls].extend({
defaults: _.extend(
_.deepClone(Backbone[cls].prototype.defaults),
{name: name},
attributes || {}
),
animations: _.extend(
_.deepClone(Backbone[cls].prototype.animations),
animations || {}
)
});
return Backbone[newCls];
}
extendSprite("Tile", "land1", {collision: true}, {idle: {sequences: [353]}});
extendSprite("Tile", "land2", {collision: true}, {idle: {sequences: [354]}});
extendSprite("Tile", "land3", {collision: true}, {idle: {sequences: [355]}});
extendSprite("Tile", "land4", {collision: true}, {idle: {sequences: [237]}});
extendSprite("Tile", "land5", {collision: true}, {idle: {sequences: [238]}});
extendSprite("Tile", "land6", {collision: true}, {idle: {sequences: [239]}});
extendSprite("Tile", "land7", {collision: true}, {idle: {sequences: [208]}});
extendSprite("Tile", "land8", {collision: true}, {idle: {sequences: [34]}});
extendSprite("Tile", "mush1", {collision: true}, {idle: {sequences: [382]}});
extendSprite("Tile", "mush2", {collision: true}, {idle: {sequences: [383]}});
extendSprite("Tile", "mush3", {collision: true}, {idle: {sequences: [384]}});
extendSprite("Tile", "mush4", {collision: true}, {idle: {sequences: [266]}});
extendSprite("Tile", "mush5", {collision: true}, {idle: {sequences: [267]}});
extendSprite("Tile", "mush6", {collision: true}, {idle: {sequences: [268]}});
extendSprite("Tile", "ground", {collision: true}, {idle: {sequences: [0]}});
extendSprite("Tile", "ground2", {collision: true}, {idle: {sequences: [31]}});
extendSprite("Tile", "block", {collision: true}, {idle: {sequences: [3]}});
extendSprite("Tile", "block2", {collision: true}, {idle: {sequences: [29]}});
extendSprite("Tile", "cloud-small", {collision: true}, {idle: {sequences: [613]}});
extendSprite("Tile", "water1", {collision: false}, {idle: {sequences: [583]}});
extendSprite("Tile", "water2", {collision: false}, {idle: {sequences: [612]}});
extendSprite("Tile", "cloud1", {collision: false}, {idle: {sequences: [580]}});
extendSprite("Tile", "cloud2", {collision: false}, {idle: {sequences: [581]}});
extendSprite("Tile", "cloud3", {collision: false}, {idle: {sequences: [582]}});
extendSprite("Tile", "cloud-happy1", {collision: false}, {idle: {sequences: [585]}});
extendSprite("Tile", "cloud-happy2", {collision: false}, {idle: {sequences: [586]}});
extendSprite("Tile", "cloud-happy3", {collision: false}, {idle: {sequences: [587]}});
extendSprite("Tile", "cloud4", {collision: false}, {idle: {sequences: [609]}});
extendSprite("Tile", "cloud5", {collision: false}, {idle: {sequences: [610]}});
extendSprite("Tile", "cloud6", {collision: false}, {idle: {sequences: [611]}});
extendSprite("Tile", "cloud-happy4", {collision: false}, {idle: {sequences: [614]}});
extendSprite("Tile", "cloud-happy5", {collision: false}, {idle: {sequences: [615]}});
extendSprite("Tile", "cloud-happy6", {collision: false}, {idle: {sequences: [616]}});
extendSprite("Tile", "cloud-platform1", {collision: true}, {idle: {sequences: [588]}});
extendSprite("Tile", "cloud-platform2", {collision: true}, {idle: {sequences: [589]}});
extendSprite("Tile", "cloud-platform3", {collision: true}, {idle: {sequences: [590]}});
extendSprite("Tile", "cloud-platform4", {collision: true}, {idle: {sequences: [704]}});
extendSprite("Tile", "cloud-platform5", {collision: true}, {idle: {sequences: [705]}});
extendSprite("Tile", "cloud-platform6", {collision: true}, {idle: {sequences: [706]}});
Backbone.Platform = Backbone.Character.extend({
defaults: {
type: "character",
name: "platform",
width: 96,
height: 32,
spriteSheet: "tiles",
tileX: 256,
tileY: 640,
state: "idle",
static: false,
collision: true
},
initialize: function(attributes, options) {
options || (options = {});
this.world = options.world;
},
update: function(dt) {
return true;
},
draw: function(context, options) {
options || (options = {});
var x = this.get("x") + (options.offsetX || 0),
y = this.get("y") + (options.offsetY || 0),
tileX = this.get("tileX"),
tileY = this.get("tileY"),
tileWidth = this.get("width"),
tileHeight = this.get("height");
context.drawImage(
this.spriteSheet.img,
tileX, tileY, tileWidth, tileHeight,
Math.round(x), Math.round(y), tileWidth, tileHeight
);
return this;
}
});
}).call(this);

BIN
games/mario/gui/arcade.ttf Normal file

Binary file not shown.

BIN
games/mario/gui/arcade.woff Normal file

Binary file not shown.

View File

@@ -0,0 +1,89 @@
(function() {
Backbone.LabelButton = Backbone.Button.extend({
defaults: _.extend({}, Backbone.Button.prototype.defaults, {
x: 400,
y: 400,
width: 160,
height: 100,
backgroundColor: "transparent",
text: "",
textPadding: 12,
textContextAttributes: {
fillStyle: "#FFC221",
font: "40px arcade",
textBaseline: "middle",
fontWeight: "normal",
textAlign: "center"
},
easing: "easeInCubic",
easingTime: 400
})
});
Backbone.Scene = Backbone.Element.extend({
defaults: _.extend({}, Backbone.Element.prototype.defaults, {
x: 0,
y: 0,
width: 960,
height: 700,
backgroundColor: "#000",
opacity: 0,
text: "",
easing: "easeInCubic",
easingTime: 400
}),
initialize: function(attributes, options) {
Backbone.Element.prototype.initialize.apply(this, arguments);
options || (options = {});
this.saved = options.saved;
this.world = options.world;
this.levels = options.levels;
this.pauseButton = options.pauseButton;
this.input = options.input;
_.bindAll(this, "enter", "exit");
},
enter: function() {
this.set("opacity", 0);
this.fadeIn();
return this;
},
exit: function() {
this.set("opacity", 1);
this.fadeOut();
return this;
}
});
Backbone.Panel = Backbone.Element.extend({
defaults: _.extend({}, Backbone.Element.prototype.defaults, {
x: 160,
y: 720,
width: 640,
height: 140,
backgroundColor: "transparent",
img: "#gui", imgX: 0, imgY: 473, imgWidth: 640, imgHeight: 480, imgMargin: 0,
text: "",
textPadding: 24,
textContextAttributes: {
fillStyle: "#FFC221",
font: "40px arcade",
textBaseline: "middle",
fontWeight: "normal",
textAlign: "center"
},
easing: "easeOutCubic",
easingTime: 600
}),
initialize: function(attributes, options) {
Backbone.Element.prototype.initialize.apply(this, arguments);
_.bindAll(this, "show");
this.set("y", Backbone.HEIGHT);
},
show: function() {
this.moveTo(this.get("x"), 100);
return this;
}
});
}).call(this);

BIN
games/mario/gui/gui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,61 @@
<!doctype html>
<html style="touch-action: none;">
<head>
<title>GUI - Backbone Game Engine</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="../favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="../apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=960, user-scalable=no"/>
<meta name="mobileoptimized" content="0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<script src="../3rd/underscore.js" type="text/javascript"></script>
<script src="../3rd/backbone.native.js" type="text/javascript"></script>
<script src="../3rd/backbone.js" type="text/javascript"></script>
<script src="../3rd/qtree.js" type="text/javascript"></script>
<script src="../src/adjust-viewport.js" type="text/javascript"></script>
<script src="../src/shapes.js" type="text/javascript"></script>
<script src="../src/core.js" type="text/javascript"></script>
<script src="elements.js" type="text/javascript"></script>
<script src="title-screen.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<style>
@-ms-viewport {
width: 960px;
}
@font-face {
font-family: 'arcade';
src: url('arcade.ttf'), url('arcade.woff');
}
body {
margin: 0;
background-color: #000;
font-family: "arcade";
line-height: 1.2em;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
</style>
</head>
<body>
<img id="title-screen" src="title-screen.jpg" style="display:none;" />
<img id="gui" src="gui.png" style="display:none;" />
<canvas id="foreground" width="960" height="720" screencanvas="true">
Your browser does not support canvas element.
</canvas>
</body>
</html>

99
games/mario/gui/main.js Normal file
View File

@@ -0,0 +1,99 @@
$(window).on("load", function() {
var NATIVE = navigator.isCocoonJS,
MOBILE = "onorientationchange" in window ||
window.navigator.msMaxTouchPoints ||
window.navigator.isCocoonJS;
var canvas = document.getElementById("foreground"),
context = canvas.getContext("2d");
if (MOBILE) {
canvas.height = Math.round(Math.min(canvas.height, canvas.width * Math.min(window.innerHeight, window.innerWidth) / Math.max(window.innerHeight, window.innerWidth) ));
} else {
canvas.height = Math.round(Math.min(canvas.height, window.innerHeight));
adjustViewport(canvas, canvas.width, canvas.height);
}
console.log("canvas.width=" + canvas.width + " canvas.height=" + canvas.height);
_.extend(Backbone, {
NATIVE: NATIVE,
MOBILE: MOBILE,
HEIGHT: canvas.height,
WIDTH: canvas.width
});
Backbone.Controller = Backbone.Model.extend({
initialize: function(attributes, options) {
options || (options = {});
var controller = this;
this.debugPanel = new Backbone.DebugPanel({}, {color: "#fff"});
this.titleScreenGui = new Backbone.TitleScreenGui({
id: "titleScreenGui",
}, {
saved: {
level: 1,
coins: 10,
time: 1000000
}
});
// The game engine
this.engine = new Backbone.Engine({}, {
canvas: canvas,
debugPanel: this.debugPanel
});
// Controls
$(document).on("keypress.Controller", function(e) {
if (e.keyCode == 66 || e.keyCode == 98)
controller.engine.toggle(); // b to break the animation
});
this.listenTo(this.engine, "play", this.play);
this.listenTo(this.engine, "nextLevel", this.nextLevel);
this.showTitleScreen();
},
showTitleScreen: function() {
this.engine.stop();
this.engine.reset();
if (this.debugPanel) this.debugPanel.clear();
// Start everything
this.engine.set("clearOnDraw", true);
this.engine.add([
this.titleScreenGui,
this.debugPanel
]);
this.engine.start();
},
play: function() {
this.engine.stop();
this.engine.reset();
if (this.debugPanel) this.debugPanel.clear();
// TO DO: start a new game. Set the state and add world, input, etc to engine
},
nextLevel: function() {
this.engine.stop();
this.engine.reset();
if (this.debugPanel) this.debugPanel.clear();
// TO DO: continue an existing game. Reset the state and add world, input, etc to engine
}
});
var controller = new Backbone.Controller();
// Expose things as globals - easier to debug
_.extend(window, {
canvas: canvas,
context: context,
controller: controller,
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -0,0 +1,258 @@
(function() {
Backbone.PullOutButton = Backbone.Button.extend({
defaults: _.extend({}, Backbone.Button.prototype.defaults, {
x: -372,
width: 372,
height: 76,
backgroundColor: "transparent",
img: "#gui", imgX: 0, imgY: 0, imgWidth: 372, imgHeight: 80, imgMargin: 0,
textPadding: 12,
textContextAttributes: {
fillStyle: "#FFC221",
font: "34px arcade",
textBaseline: "middle",
fontWeight: "normal",
textAlign: "right"
},
easing: "easeOutCubic",
easingTime: 600
})
});
Backbone.SavedGame = Backbone.Element.extend({
defaults: _.extend({}, Backbone.Element.prototype.defaults, {
x: 960,
y: 280,
width: 333,
height: 80,
backgroundColor: "transparent",
img: "#gui", imgX: 0, imgY: 225, imgWidth: 333, imgHeight: 247, imgMargin: 0,
text: "High Score",
textPadding: 24,
textContextAttributes: {
fillStyle: "#FFC221",
font: "34px arcade",
textBaseline: "middle",
fontWeight: "normal",
textAlign: "left"
},
easing: "easeOutCubic",
easingTime: 600
}),
initialize: function(attributes, options) {
Backbone.Element.prototype.initialize.apply(this, arguments);
this.saved = options.saved;
},
onAttach: function() {
Backbone.Element.prototype.onAttach.apply(this, arguments);
this.set("text", "Level " + (this.saved ? this.saved.level : "?"));
},
onDraw: function(context) {
var x = this.get("x"),
y = this.get("y"),
coins = this.saved ? this.saved.coins : "?",
time = this.saved ? _.ms2time(this.saved.time) : "?";
context.font = "30px arcade";
context.fillStyle = "#FFF";
context.textBaseline = this.attributes.textContextAttributes.textBaseline;
context.fontWeight = this.attributes.textContextAttributes.fontWeight;
context.textAlign = this.attributes.textContextAttributes.textAlign;
context.fillText(coins, x+80, y+105);
context.fillText(time, x+80, y+170);
}
});
Backbone.Credits = Backbone.Panel.extend({
defaults: _.extend({}, Backbone.Panel.prototype.defaults, {
text: "",
textContextAttributes: {
fillStyle: "#FFC221",
font: "40px arcade",
textBaseline: "middle",
fontWeight: "normal",
textAlign: "center"
}
}),
onDraw: function(context, options) {
var b = this.toJSON(),
y = b.y;
// Titles
b.textContextAttributes.font = "30px arcade";
b.textContextAttributes.fillStyle = "#FFC221";
b.text = "Graphics";
b.y = y;
this.drawText(b, context, options);
b.text = "Testing";
b.y = y + 130;
this.drawText(b, context, options);
b.text = "Story & Coding";
b.y = y + 230;
this.drawText(b, context, options);
// Content
b.textContextAttributes.font = "24px arcade";
b.textContextAttributes.fillStyle = "#FFF";
b.text = "???";
b.y = y + 40;
this.drawText(b, context, options);
b.text = "???";
b.y = y + 170;
this.drawText(b, context, options);
b.text = "???";
b.y = y + 270;
this.drawText(b, context, options);
b.textContextAttributes.fillStyle = "#DDD";
b.text = "Built with Backbone Game Engine";
b.y = y + 340;
this.drawText(b, context, options);
}
});
Backbone.TitleScreenGui = Backbone.Scene.extend({
defaults: _.extend({}, Backbone.Scene.prototype.defaults, {
img: "#title-screen",
imgWidth: 960,
imgHeight: 700
}),
initialize: function(attributes, options) {
Backbone.Scene.prototype.initialize.apply(this, arguments);
this.banner = new Backbone.Button({
x: 0, y: 240,
width: 960, height: 145,
backgroundColor: "transparent",
img: "#gui", imgX: 0, imgY: 80, imgWidth: 960, imgHeight: 144, imgMargin: 5,
easing: "easeInOutQuad",
easingTime: 400
});
this.touchStart = new Backbone.LabelButton({
text: "Touch to start",
opacity: 0
});
this.loading = new Backbone.LabelButton({
y: Backbone.HEIGHT,
text: "Loading...",
easingTime: 300
});
this.play = new Backbone.PullOutButton({
y: Backbone.HEIGHT - 300,
text: "New Game "
});
this.play.on("tap", _.partial(this.action, "play"), this);
this.showCredits = new Backbone.PullOutButton({
y: Backbone.HEIGHT - 100,
text: "Credits "
});
this.savedGame = new Backbone.SavedGame({
y: Backbone.HEIGHT - 300
}, {
saved: this.saved
});
this.credits = new Backbone.Credits();
this.showCredits.on("tap", _.partial(this.showPanel, this.credits), this);
},
postInitialize: function() {
this.listenTo(this.engine, "tap", this.onTouchStart);
// Hack to avoid FOUT
this.touchStart.set("opacity", 1);
this.play.textMetrics = undefined;
this.showCredits.textMetrics = undefined;
},
onAttach: function() {
Backbone.Scene.prototype.onAttach.apply(this, arguments);
this.stopListening(this.engine);
this.set("opacity", 1);
this.loading.set("x", Backbone.HEIGHT);
this.play.set("text", this.saved ? "Continue " : "New Game ");
this.engine.add([this.banner, this.touchStart, this.loading, this.play, this.showCredits, this.credits, this.savedGame]);
if (!this.ready)
setTimeout(this.postInitialize.bind(this), 200);
else
setTimeout(this.showButtons.bind(this), 100);
},
onDetach: function() {
Backbone.Scene.prototype.onDetach.apply(this, arguments);
this.engine.remove([this.banner, this.touchStart, this.loading, this.play, this.showCredits, this.credits, this.savedGame]);
},
onTouchStart: function(e) {
// Animate some stuff
this.banner.moveTo(this.banner.get("x"), 50);
this.touchStart.moveTo(this.touchStart.get("x"), Backbone.HEIGHT);
this.stopListening(this.engine);
this.ready = true;
this.showButtons();
},
showButtons: function() {
this.play.moveTo(-this.play.get("width") + this.play.textMetrics.width + this.play.get("textPadding")*2, this.play.get("y"));
this.showCredits.moveTo(-this.showCredits.get("width") + this.showCredits.textMetrics.width + this.showCredits.get("textPadding")*2, this.showCredits.get("y"));
if (this.saved)
this.savedGame.moveTo(720, this.savedGame.get("y"));
},
hideButtons: function() {
this.play.moveTo(-this.play.get("width"), this.play.get("y"));
this.showCredits.moveTo(-this.showCredits.get("width"), this.showCredits.get("y"));
this.savedGame.moveTo(960, this.savedGame.get("y"));
},
showPanel: function(panel) {
this.panel = panel;
this.panel.moveTo(this.panel.get("x"), 50);
this.hideButtons();
this.listenTo(this.engine, "tap", this.hidePanel);
},
hidePanel: function() {
this.stopListening(this.engine);
this.panel.moveTo(this.panel.get("x"), Backbone.HEIGHT);
this.panel = undefined;
this.showButtons();
},
update: function(dt) {
if (!Backbone.Scene.prototype.update.apply(this, arguments)) return false;
var attrs = {opacity: this.get("opacity")},
options = {silent: true};
this.banner.set(attrs, options);
this.touchStart.set(attrs, options);
this.loading.set(attrs, options);
this.play.set(attrs, options);
this.showCredits.set(attrs, options);
this.credits.set(attrs, options);
this.savedGame.set(attrs, options);
return true;
},
action: function(event) {
if (event == "play") this.loading.set("x", 400);
var gui = this;
this.hideButtons();
setTimeout(function() {
gui.fadeOut(function() {
gui.engine.trigger(event);
});
}, 400);
}
});
}).call(this);

1870
games/mario/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
<!doctype html>
<html style="touch-action: none;">
<head>
<title>Mario - Backbone Game Engine</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="../favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="../apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=960, user-scalable=no"/>
<meta name="mobileoptimized" content="0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<script src="../3rd/qtree.js" type="text/javascript"></script>
<script src="../3rd/underscore.js" type="text/javascript"></script>
<script src="../3rd/backbone.native.js" type="text/javascript"></script>
<script src="../3rd/backbone.js" type="text/javascript"></script>
<script src="../src/adjust-viewport.js" type="text/javascript"></script>
<script src="../src/shapes.js" type="text/javascript"></script>
<script src="../src/core.js" type="text/javascript"></script>
<script src="../src/input.js" type="text/javascript"></script>
<script src="../src/character.js" type="text/javascript"></script>
<script src="../src/hero.js" type="text/javascript"></script>
<script src="../src/world.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<style>
body {
margin: 0;
background-color: #000;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
</style>
</head>
<body>
<img id="mario" src="super-mario-2x.png" style="display:none;" />
<canvas id="foreground" width="960" height="700">
Your browser does not support canvas element.
</canvas>
</body>
</html>

75
games/mario/mario/main.js Normal file
View File

@@ -0,0 +1,75 @@
$(window).on("load", function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
// Mario alone in an empty world. Control him with the touchpad.
Backbone.Mario = Backbone.Hero.extend({
defaults: _.extend({}, Backbone.Hero.prototype.defaults, {
name: "mario",
spriteSheet: "mario"
})
});
var canvas = document.getElementById("foreground");
adjustViewport(canvas);
var spriteSheets = new Backbone.SpriteSheetCollection([{
id: "mario",
img: "#mario",
tileWidth: 32,
tileHeight: 64,
tileColumns: 21,
tileRows: 6
}]).attachToSpriteClasses();
var debugPanel = new Backbone.DebugPanel();
var input = new Backbone.Input({
drawTouchpad: true,
drawPause: true
});
var mario = new Backbone.Mario({
x: 400, y: 200, floor: 500
}, {
input: input
});
var world = new Backbone.World({
width: 30, height: 17,
tileWidth: 32, tileHeight: 32,
viewportBottom: 156,
backgroundColor: "rgba(66, 66, 255, 1)"
}, {
input: input
});
world.add(mario);
var engine = new Backbone.Engine({}, {
canvas: canvas,
debugPanel: debugPanel,
input: input
});
engine.add([
world,
input,
debugPanel
]);
// Expose things as globals - easier to debug
_.extend(window, {
canvas: canvas,
engine: engine,
world: world,
mario: mario
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,64 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
// Ensures the canvas is always full size and/or centered.
//
// On mobile:
// Works in conjunction with meta tag viewport where width is set to the canvas width.
// <meta name="viewport" content="width=960, user-scalable=no"/>
// Will modify the canvas height to fit the device aspect ratio, never exceeding the
// origin canvas height.
// Set keepRatio to true to maintain your aspect ratio. In such a case, the viewport
// is adjusted to the height of the canvas. The canvas is kept centered on screen (with
// black bars left and right to fill empty space).
//
// On Desktop:
// The viewport meta tag is ignored. Instead, the canvas is kept centered. The height
// may be reduced if the window height is less than the canvas (to avoid scrolling).
// Set keepRatio to maintain the canvas height.
function adjustViewport(canvas, keepRatio) {
var viewport = typeof document.querySelector == "function" ? document.querySelector("meta[name=viewport]") : null,
mobile = "onorientationchange" in window ||
window.navigator.msMaxTouchPoints ||
window.navigator.isCocoonJS;
function onResize() {
if (window.innerWidth > window.innerHeight) {
// Landscape
canvas.style.left = _.max([0, (window.innerWidth - canvas.width) / 2]) + "px";
if (mobile && viewport)
viewport.setAttribute("content", "width=" + Math.ceil(canvas.height * window.innerWidth / window.innerHeight) + ",user-scalable=no");
} else {
// Portrait
canvas.style.left = "0px";
if (mobile && viewport)
viewport.setAttribute("content", "width=" + canvas.width + ",user-scalable=no");
}
}
if (mobile && !keepRatio) {
canvas.height = Math.round(Math.min(canvas.height, canvas.width * Math.min(window.innerHeight, window.innerWidth) / Math.max(window.innerHeight, window.innerWidth) ));
} else {
if (!keepRatio)
canvas.height = Math.round(Math.min(canvas.height, window.innerHeight));
window.addEventListener("resize", _.debounce(onResize, 300));
setTimeout(onResize, 10);
}
}
_.extend(window, {
adjustViewport: adjustViewport
});
}).call(this);

84
games/mario/src/camera.js Normal file
View File

@@ -0,0 +1,84 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
// Camera class
// Ensures the hero is always in the viewport.
// Properly pans the world.
Backbone.Camera = Backbone.Model.extend({
defaults: {
left: 200,
right: 400,
top: 100,
bottom: 100
},
initialize: function(attributes, options) {
this.setOptions(options || {});
},
setOptions: function(options) {
options || (options = {});
_.extend(this, options || {});
this.stopListening();
if (this.subject && this.world)
this.listenTo(this.subject, "change:x change:y", this.maybePan);
},
maybePan: function() {
if (!this.world) return this;
var w = this.world.toShallowJSON(),
worldX = w.x,
worldY = w.y,
worldWidth = w.width * w.tileWidth,
worldHeight = w.height * w.tileHeight,
viewportWidth = this.world.viewport.width,
viewportHeight = this.world.viewport.height,
subjectX = this.subject.get("x") + w.x,
subjectY = this.subject.get("y") + w.y,
subjectWidth = this.subject.get("tileWidth"),
subjectHeight = this.subject.get("tileHeight"),
left = this.get("left") + w.viewportLeft,
right = w.viewportLeft + viewportWidth - this.get("right"),
top = this.get("top") + w.viewportTop,
bottom = w.viewportTop + viewportHeight - this.get("bottom");
if (subjectX < left && w.x < 0) {
// Pan right (to see more left)
worldX = w.x + (left - subjectX);
if (worldX > 0) worldX = 0;
} else if (subjectX > right && w.x + worldWidth > viewportWidth) {
// Pan left (to see more right)
worldX = w.x - (subjectX - right);
if (worldX + worldWidth < viewportWidth)
worldX = -worldWidth + viewportWidth;
}
if (subjectY < top && w.y < 0) {
// Pan down (to see more up)
worldY = w.y + (top - subjectY);
if (worldY > 0) worldY = 0;
} else if (subjectY > bottom && w.y + worldHeight > viewportHeight) {
// Pan up (to see more down)
worldY = w.y - (subjectY - bottom);
if (worldY + worldHeight < viewportHeight)
worldY = -worldHeight + viewportHeight;
}
if (worldX != w.x || worldY != w.y)
this.world.set({x: worldX, y: worldY});
},
update: function(dt) {
return false;
},
draw: function(context) {
return this;
}
});
}).call(this);

View File

@@ -0,0 +1,512 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var sequenceDelay = 300,
walkVelocity = 50,
fallAcceleration = 1200,
fallVelocity = 600;
var animations = {
"idle-left": {
sequences: [0],
delay: sequenceDelay,
velocity: 0,
scaleX: 1,
scaleY: 1
},
"idle-right": {
sequences: [0],
delay: sequenceDelay,
velocity: 0,
scaleX: -1,
scaleY: 1
},
"walk-left": {
sequences: [1, 0],
delay: sequenceDelay,
velocity: -walkVelocity,
scaleX: 1,
scaleY: 1
},
"walk-right": {
sequences: [1, 0],
delay: sequenceDelay,
velocity: walkVelocity,
scaleX: -1,
scaleY: 1
},
"fall-left": {
sequences: [0],
delay: sequenceDelay,
velocity: -walkVelocity,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration,
scaleX: 1,
scaleY: 1
},
"fall-right": {
sequences: [0],
delay: sequenceDelay,
velocity: walkVelocity,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration,
scaleX: -1,
scaleY: 1
},
"ko-left": {
sequences: [0],
delay: sequenceDelay,
velocity: -walkVelocity,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration,
scaleX: 1,
scaleY: -1
},
"ko-right": {
sequences: [0],
delay: sequenceDelay,
velocity: walkVelocity,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration,
scaleX: -1,
scaleY: -1
}
};
var hurtAnimation = {
sequences: [0],
delay: 300,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration
};
animations["idle-hurt-left"] = _.extend({}, animations["idle-left"], hurtAnimation);
animations["idle-hurt-right"] = _.extend({}, animations["idle-right"], hurtAnimation);
animations["walk-hurt-left"] = _.extend({}, animations["walk-left"], hurtAnimation);
animations["walk-hurt-right"] = _.extend({}, animations["walk-right"], hurtAnimation);
animations["fall-hurt-left"] = _.extend({}, animations["fall-left"], hurtAnimation);
animations["fall-hurt-right"] = _.extend({}, animations["fall-right"], hurtAnimation);
Backbone.Character = Backbone.Sprite.extend({
defaults: _.extend({}, Backbone.Sprite.prototype.defaults, {
name: "character",
type: "character",
width: 32,
height: 32,
zIndex: 1,
spriteSheet: undefined,
state: "walk-left",
velocity: 0,
yVelocity: 0,
collision: true,
static: false,
visible: true,
health: 1,
attackDamage: 1,
floor: null,
ceiling: null,
aiDelay: 1000
}),
animations: animations,
initialize: function(attributes, options) {
Backbone.Sprite.prototype.initialize.apply(this, arguments);
options || (options = {});
this.on("attach", this.onAttach, this);
this.on("detach", this.onDetach, this);
this.on("hit", this.hit, this);
this.on("change:health", this.onHealthChange, this);
this.on("beforeFall", this.onBeforeFall, this);
},
onAttach: function() {
if (!this.engine) return;
this.onDetach();
},
onDetach: function() {
},
onBeforeFall: function() {
return this;
},
onHealthChange: function(model, health, options) {
options || (options = {});
var cur = this.getStateInfo(),
dir = options.dir || cur.dir,
opo = dir == "left" ? "right" : "left";
if (health == 0)
return this.knockout(options.sprite || null, dir, options.dir2 || null);
else if (health < this.previous("health"))
return this.hurt(options.sprite || null, dir, options.dir2 || null);
this.lastAIEvent = _.now();
return this;
},
knockout: function(sprite, dir) {
var opo = dir == "left" ? "right" : "left",
state = this.buildState("ko", opo);
this.whenAnimationEnds = null;
this.set({
state: state,
velocity: this.animations[state].velocity,
yVelocity: -this.animations[state].yVelocity,
sequenceIndex: 0,
collision: false
});
this.cancelUpdate = true;
return this;
},
hurt: function(sprite, dir) {
this.whenAnimationEnds = null;
this.set({
state: this.buildState("fall", "hurt", dir),
velocity: this.animations["ko-"+dir].velocity,
yVelocity: -this.animations["ko-"+dir].yVelocity,
sequenceIndex: 0
});
return this;
},
hit: function(sprite, dir, dir2) {
if (this._handlingSpriteHit) return this;
this._handlingSpriteHit = sprite;
var cur = this.getStateInfo();
if (cur.mov2 == "hurt") return this;
if (dir2 == "attack") {
this.cancelUpdate = true;
var attackDamage = sprite.get("attackDamage") || 1;
this.set({health: Math.max(this.get("health") - attackDamage, 0)}, {sprite: sprite, dir: dir, dir2: dir2});
} else if (cur.dir == dir && cur.mov2 == null) {
this.cancelUpdate = true;
this.set("state", this.buildState(cur.mov, cur.opo));
}
tis._handlingSpriteHit = undefined;
return this;
},
startNewAnimation: function(state, attrs, done) {
this.lastSequenceChangeTime = _.now();
this.set(_.extend({
state: state,
sequenceIndex: 0
}, attrs));
this.whenAnimationEnds = done;
return this;
},
updateSequenceIndex: function(dt) {
var sequenceIndex = this.get("sequenceIndex"),
animation = this.getAnimation(),
delay = animation.delay || 0,
now = _.now(),
triggerAnimationEnd = false;
if (!animation.sequences) {
sequenceIndex = 0;
this.lastSequenceChangeTime = now;
} else if (sequenceIndex >= animation.sequences.length) {
sequenceIndex = 0;
this.lastSequenceChangeTime = now;
triggerAnimationEnd = true;
} else if (delay && now > this.lastSequenceChangeTime + delay) {
sequenceIndex = sequenceIndex < animation.sequences.length-1 ? sequenceIndex + 1 : 0;
this.lastSequenceChangeTime = now;
if (sequenceIndex == 0) triggerAnimationEnd = true;
}
if (triggerAnimationEnd && typeof this.whenAnimationEnds == "function") {
this.whenAnimationEnds.call(this);
this.whenAnimationEnds = null;
}
return sequenceIndex;
},
ai: function(dt) {
return this;
},
update: function(dt) {
// Movements are only possible inside a world
if (!this.world) return true;
this.cancelUpdate = false;
// Velocity and state
var self = this,
velocity = this.get("velocity") || 0,
yVelocity = this.get("yVelocity") || 0,
x = this.get("x"),
y = this.get("y"),
state = this.get("state"),
cur = this.getStateInfo(),
animation = this.getAnimation(),
now = _.now(),
aiDelay = this.get("aiDelay"),
attrs = {};
// Handle AI
if (!this.lastAIEvent)
this.lastAIEvent = now;
else if (now > this.lastAIEvent + aiDelay) {
this.ai(now - this.lastAIEvent);
this.lastAIEvent = now;
if (this.cancelUpdate) return true;
}
if ((cur.mov == "ko" || cur.mov2 == "hurt") &&
this.get("sequenceIndex") == animation.sequences.length-1) {
// No sequence change - stay on last one
} else {
attrs.sequenceIndex = this.updateSequenceIndex();
}
if (velocity != animation.velocity) velocity = animation.velocity || 0;
if (cur.mov == "fall" || cur.mov == "ko" || cur.mov2 == "hurt") {
if (yVelocity < animation.yVelocity)
yVelocity += animation.yAcceleration * (dt/1000);
if (yVelocity >= animation.yVelocity)
yVelocity = animation.yVelocity;
attrs.yVelocity = yVelocity;
}
// Collision detection
var collision = this.get("collision"),
tileWidth = this.get("width"),
tileHeight = this.get("height"),
paddingLeft = this.get("paddingLeft"),
paddingRight = this.get("paddingRight"),
paddingBottom = this.get("paddingBottom"),
paddingTop = this.get("paddingTop"),
charWidth = tileWidth - paddingLeft - paddingRight,
charHeight = tileHeight - paddingTop - paddingBottom,
charLeftX = Math.round(x + velocity * (dt/1000)) + paddingLeft,
charRightX = charLeftX + charWidth,
bottomWorld = this.world.height() + tileHeight,
relativeVelocity = 0,
bottomY = _.minNotNull([
this.get("floor"),
bottomWorld
]);
var charBottomY, charTopY,
bottomPlatform, sprite, i, type;
function updateTopBottom() {
charBottomY = Math.round(y + yVelocity * (dt/1000)) + tileHeight - paddingBottom,
charTopY = charBottomY - charHeight,
self.buildCollisionMap(charTopY, charRightX, charBottomY, charLeftX);
if (collision)
self.world.findCollisions(self.collisionMap, null, self, true);
}
updateTopBottom();
for (i = 0; i < this.collisionMap.bottom.sprites.length; i++) {
sprite = this.collisionMap.bottom.sprites[i];
type = sprite.get("type")
if (type == "tile" || type == "platform")
bottomY = Math.min(bottomY, sprite.getTop(true));
if (type == "platform") bottomPlatform = sprite;
}
if (yVelocity >= 0) {
// Walking or Falling...
if (charBottomY >= bottomY) {
if (charBottomY >= bottomWorld) {
this.world.remove(this);
return false;
}
for (i = 0; i < this.collisionMap.bottom.sprites.length; i++)
if (cur.mov != "ko")
this.collisionMap.bottom.sprites[i].trigger("hit", this, "top");
if (this.cancelUpdate) return this;
// Stop falling because obstacle below
attrs.yVelocity = yVelocity = 0;
attrs.y = y = bottomY - tileHeight + paddingBottom;
if (cur.mov == "fall")
attrs.state = this.buildState("walk", cur.mov2, cur.dir);
else if (cur.mov == "ko") {
attrs.velocity = velocity = 0;
}
updateTopBottom();
if (charBottomY == bottomY && bottomPlatform)
relativeVelocity = bottomPlatform.get("velocity");
} else if (cur.mov != "fall" && cur.mov != "ko" && charBottomY < bottomY) {
// Start falling if no obstacle below
attrs.state = this.buildState("fall", cur.mov2, cur.dir);
if (cur.mov == "walk" && velocity != 0) {
this.trigger("beforeFall");
if (this.cancelUpdate) return true;
}
}
} else if (yVelocity < 0) {
// Jumping
var topY = -400;
for (i = 0; i < this.collisionMap.top.sprites.length; i++) {
sprite = this.collisionMap.top.sprites[i];
if (sprite.get("type") == "tile")
topY = Math.max(topY, sprite.getBottom(true));
}
if (charTopY < topY) {
attrs.yVelocity = yVelocity = 0;
charTopY = topY;
charBottomY = topY + charHeight;
attrs.y = y = charBottomY - tileHeight;
updateTopBottom();
}
}
// When not in play mode, do not allow horizontal displacements or animations
if (this.world.get("state") != "play") {
attrs.velocity = velocity = 0;
attrs.sequenceIndex = this.get("sequenceIndex");
} else {
// Walls and other obstacles
if (velocity <= 0 && collision) {
// Turn around if obstacle left
var worldLeft = -tileWidth,
leftX = worldLeft,
leftCharacter;
if (cur.mov != "ko" && cur.mov != "idle")
for (i = 0; i < this.collisionMap.left.sprites.length; i++) {
sprite = this.collisionMap.left.sprites[i];
leftX = Math.max(leftX, sprite.getRight(true));
if (sprite.get("type") == "character" &&
(!leftCharacter || sprite.getRight(true) > leftCharacter.getRight(true)))
leftCharacter = sprite;
}
if (charLeftX <= leftX) {
if (charLeftX <= worldLeft) {
this.world.remove(this);
return false;
}
if (leftCharacter && cur.mov2 != "hurt") {
leftCharacter.trigger("hit", this, "right", cur.mov2);
if (this.cancelUpdate) return true;
}
attrs.velocity = velocity = velocity * -1;
attrs.state = this.buildState(cur.mov, cur.mov2, cur.opo);
attrs.x = x = leftX - paddingLeft;
}
}
if (velocity >= 0 && collision) {
// Turn around if obstacle to the right
var worldRight = this.world.width(),
rightX = worldRight,
rightCharacter;
if (cur.mov != "ko" && cur.mov != "idle")
for (i = 0; i < this.collisionMap.right.sprites.length; i++) {
sprite = this.collisionMap.right.sprites[i];
rightX = Math.min(rightX, sprite.getLeft(true));
if (sprite.get("type") == "character" &&
(!rightCharacter || sprite.getLeft(true) < rightCharacter.getLeft(true)))
rightCharacter = sprite;
}
if (charRightX >= rightX) {
if (charRightX >= worldRight) {
this.world.remove(this);
return false;
}
if (rightCharacter && cur.mov2 != "hurt") {
rightCharacter.trigger("hit", this, "left", cur.mov2);
if (this.cancelUpdate) return true;
}
attrs.velocity = velocity = velocity * -1;
attrs.state = this.buildState(cur.mov, cur.mov2, cur.opo);
attrs.x = x = rightX - charWidth - paddingLeft;
}
}
}
if (velocity || relativeVelocity) attrs.x = x = x + Math.round((velocity + relativeVelocity) * (dt/1000));
if (yVelocity) attrs.y = y = y + Math.round(yVelocity * (dt/1000));
// Set modified attributes
if (!_.isEmpty(attrs)) this.set(attrs);
if (typeof this.onUpdate == "function") return this.onUpdate(dt);
return true;
},
toggleDirection: function(dirIntent) {
var cur = this.getStateInfo();
this.set({state: this.buildState(cur.mov, cur.mov2, dirIntent)});
return this;
},
getStateInfo: function(state) {
var state = state || this.get("state"),
pieces = state.split("-");
if (pieces.length < 2) return {
state: state,
mov: state
};
var stateInfo = {};
stateInfo.mov = pieces[0];
stateInfo.mov2 = pieces.length == 3 ? pieces[1] : null;
stateInfo.dir = pieces.length == 3 ? pieces[2] : pieces[1];
stateInfo.opo = stateInfo.dir == "right" ? "left" : "right";
return stateInfo;
},
isAttacking: function() {
if (this.cancelUpdate) return false;
var cur = this.getStateInfo();
return cur.mov2 == "attack";
},
buildState: function(piece1, piece2, piece3) {
var state = "";
if (piece1) state += piece1;
if (piece2) state += (state.length ? "-" : "") + piece2;
if (piece3) state += (state.length ? "-" : "") + piece3;
return state;
},
buildCollisionMap: function(top, right, bottom, left) {
this.collisionMap || (this.collisionMap = {
right: {x: 0, y: 0, dir: "right", sprites: [], sprite: null},
left: {x: 0, y: 0, dir: "left", sprites: [], sprite: null},
bottom: {x: 0, y: 0, dir: "bottom", sprites: [], sprite: null},
top: {x: 0, y: 0, dir: "top", sprites: [], sprite: null}
});
var width = right - left,
height = bottom - top;
this.collisionMap.left.x = left;
this.collisionMap.right.x = right;
this.collisionMap.left.y = this.collisionMap.right.y = top + height*0.20;
this.collisionMap.top.x = this.collisionMap.bottom.x = left + width*0.20;
this.collisionMap.top.y = top;
this.collisionMap.bottom.y = bottom;
this.collisionMap.left.height = this.collisionMap.right.height = height*0.60;
this.collisionMap.left.width = this.collisionMap.right.width = 0;
this.collisionMap.top.width = this.collisionMap.bottom.width = width*0.60;
this.collisionMap.top.height = this.collisionMap.bottom.height = 0;
for (var m in this.collisionMap)
if (this.collisionMap.hasOwnProperty(m)) {
this.collisionMap[m].sprites.length = 0;
this.collisionMap[m].sprite = null;
}
}
});
}).call(this);

1303
games/mario/src/core.js Normal file

File diff suppressed because it is too large Load Diff

324
games/mario/src/editor.js Normal file
View File

@@ -0,0 +1,324 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var pageImg = new Image();
pageImg.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABYSURBVEhLYxheoB6I1SFM0gFIsycQ/wdikg1B1kyWISDFyJpHDRk1BBugqiGpUJosQ2CaQIbsh/JBNFGAIm+Mah7VTBSguFQGAYo0wwBFmkGAIs1kAgYGANR+XZBqOruRAAAAAElFTkSuQmCC";
var drawSpriteFn = function(context, options) {
options || (options = {});
var animation = this.getAnimation(),
sequenceIndex = this.get("sequenceIndex") || 0;
if (!animation || animation.sequences.length == 0) return;
if (sequenceIndex >= animation.sequences.length) sequenceIndex = 0;
var sequence = animation.sequences[sequenceIndex]
frameIndex = _.isNumber(sequence) ? sequence : sequence.frame,
frame = this.spriteSheet.frames[frameIndex];
var width = options.tileWidth,
height = options.tileHeight;
if (this.attributes.width > this.attributes.height && this.attributes.width > options.tileWidth) {
height = this.attributes.height * options.tileWidth / this.attributes.width;
} else if (this.attributes.height > this.attributes.width && this.attributes.height > options.tileHeight) {
width = this.attributes.width * options.tileHeight / this.attributes.height;
}
context.drawImage(
this.spriteSheet.img,
frame.x, frame.y, frame.width, frame.height,
this.get("x"), this.get("y"), width, height
);
if (typeof this.onDraw == "function") this.onDraw(context, options);
return this;
};
// World Editor
// Allows the user to place tiles and characters in the World.
Backbone.WorldEditor = Backbone.Model.extend({
defaults: {
x: 136,
y: 550,
width: 820,
height: 140,
tileWidth: 32,
tileHeight: 32,
padding: 1,
backgroundColor: "#333",
selectColor: "#f00",
selected: undefined,
spriteNames: [],
page: 0,
pages: 1
},
initialize: function(attributes, options) {
options || (options = {});
if (!attributes || !attributes.spriteNames) throw "Missing attribute spriteNames";
var defs = [];
if (attributes.spriteNames.length && typeof attributes.spriteNames[0] == "object") {
for (var page = 0; page < attributes.spriteNames.length; page++)
for (var s = 0; s < attributes.spriteNames[page].length; s++)
defs.push({name: attributes.spriteNames[page][s], page: page});
this.set("pages", attributes.spriteNames.length);
} else {
defs = _.map(attributes.spriteNames, function(name) {
return {name: name, page: 0};
});
}
this.sprites = new Backbone.SpriteCollection(defs);
this.sprites.each(function(sprite) {
sprite.draw = drawSpriteFn;
});
this.world = options.world;
if (!this.world && !_.isFunction(this.world.add))
throw "Missing or invalid world option.";
this.changePageButton = new Backbone.Button({
width: 32, height: 50, borderRadius: 2,
img: pageImg, imgX: 0, imgY: 0, imgWidth: 16, imgHeight: 32, imgMargin: 10
});
this.changePageButton.on("tap", this.changePage, this);
this.debugPanel = options.debugPanel;
_.bindAll(this,
"onTap", "onDragStart", "onDrag", "onDragEnd",
"getSelectedSprite", "onMouseMove"
);
this.on("attach", this.onAttach);
this.on("detach", this.onDetach);
var editor = this;
this.on("change:page", function() {
editor.set("selected", undefined);
});
},
onAttach: function() {
var world = this.world,
engine = this.engine;
this.onDetach();
this.listenTo(this.engine, "tap", this.onTap);
this.listenTo(this.engine, "dragstart", this.onDragStart);
this.listenTo(this.engine, "dragmove", this.onDrag);
this.listenTo(this.engine, "dragend", this.onDragEnd);
$(document).on("mousemove.Edit", this.onMouseMove);
this.sprites.each(function(sprite) {
if (sprite.attributes.type == "tile" || sprite.attributes.type == "character") {
sprite.engine = engine;
sprite.trigger("attach", engine);
}
});
if (this.get("pages") > 1) {
this.changePageButton.engine = engine;
this.changePageButton.trigger("attach");
}
this.positionSprites();
},
onDetach: function() {
this.stopListening(this.engine);
$(document).off(".Edit");
this.sprites.each(function(sprite) {
if (sprite.attributes.type == "tile" || sprite.attributes.type == "character") {
sprite.engine = undefined;
sprite.trigger("detach");
}
});
this.changePageButton.trigger("detach");
},
changePage: function() {
var page = this.get("page") + 1;
if (page >= this.get("pages")) page = 0;
this.set("page", page);
},
positionSprites: function() {
var sp = this.toJSON(),
x = sp.x + sp.tileWidth + 4*sp.padding,
y = sp.y + 2*sp.padding,
page = 0;
this.sprites.each(function(sprite) {
if (sprite.attributes.page > page) {
page = sprite.attributes.page;
x = sp.x + sp.tileWidth + 4*sp.padding;
y = sp.y + 2*sp.padding;
}
sprite.set({x: x, y: y});
x += sp.tileWidth + 2*sp.padding;
if (x >= sp.x + sp.width - 2) {
x = sp.x + 2*sp.padding;
y += sp.tileHeight + 2*sp.padding;
}
});
if (this.get("pages") > 1)
this.changePageButton.set({
x: sp.x + sp.width - this.changePageButton.get("width"),
y: sp.y + sp.height - this.changePageButton.get("height")
});
return this;
},
update: function(dt) {
for (var i = 0; i < this.sprites.models.length; i++)
this.sprites.models[i].update(dt);
this.changePageButton.update(dt);
return true;
},
draw: function(context) {
var sp = this.toJSON();
// Fill background
drawRect(context, sp.x, sp.y, sp.width, sp.height, sp.backgroundColor);
// Highlight selected sprite
var st = this.sprites.findWhere({name: sp.selected}),
sx = st ? st.get("x") - 2 : sp.x,
sy = st ? st.get("y") - 2 : sp.y,
sw = sp.tileWidth + 4,
sh = sp.tileHeight + 4;
drawRect(context, sx, sy, sw, sh, sp.selectColor);
// Draw sprites
this.sprites.each(function(sprite) {
if (sprite.attributes.page == sp.page) sprite.draw(context, sp);
});
// Highlight tile position (on desktop)
if (this.mx != undefined && this.my != undefined) {
var tileWidth = this.world.get("tileWidth"),
tileHeight = this.world.get("tileHeight"),
x = this.mx - this.mx % tileWidth + this.world.get("x"),
y = this.my - this.my % tileHeight + this.world.get("y");
context.save();
context.rect(
this.world.get("viewportLeft"),
this.world.get("viewportTop"),
context.canvas.width - this.world.get("viewportRight"),
context.canvas.height - this.world.get("viewportBottom")
);
context.clip();
context.beginPath();
context.strokeStyle = "#FF0000";
context.setLineDash([5,2]);
context.rect(x, y, tileWidth, tileHeight);
context.stroke();
context.restore();
}
if (this.get("pages") > 1)
this.changePageButton.draw(context);
},
getSelectedSprite: function() {
var selected = this.get("selected");
if (!selected) return null
return this.sprites.findWhere({name: selected});
},
onTap: function(e) {
if (e.target != this.engine.canvas || e.canvasHandled) return;
var editor = this,
sp = this.toJSON(),
x = e.canvasX,
y = e.canvasY;
// Sprite selection?
if (x >= sp.x && y >= sp.y && x <= sp.x + sp.width && y <= sp.y + sp.height) {
editor.set({selected: null});
this.sprites.each(function(sprite) {
var s = sprite.toJSON();
if (s.page == sp.page && x >= s.x && y >= s.y && x <= s.x + sp.tileWidth && y <= s.y + sp.tileHeight) {
editor.set({selected: s.name});
return false;
}
});
return;
}
// Sprite placement
if (y < sp.y) {
var sprite = this.getSelectedSprite();
x -= this.world.get("x");
y -= this.world.get("y");
this.world.cloneAtPosition(sprite, x, y);
}
},
// Pan the world
onDragStart: function(e) {
var world = this.world,
sp = this.toJSON(),
x = e.canvasX,
y = e.canvasY;
if (x >= sp.x && y >= sp.y && x <= sp.x + sp.width && y <= sp.y + sp.height) return;
world.startDragWorldX = world.get("x");
world.startDragWorldY = world.get("y");
},
onDrag: function (e) {
var world = this.world;
if (!_.isNumber(world.startDragWorldX) || !_.isNumber(world.startDragWorldX)) return false;
var x = world.startDragWorldX + e.canvasDeltaX,
y = world.startDragWorldY + e.canvasDeltaY;
if (x > 0) {
x = 0;
} else {
var min = -(world.get("width") * world.get("tileWidth") - world.engine.canvas.width);
if (x < min) x = min;
}
if (y > 0) {
y = 0;
} else {
var min = -(world.get("height") * world.get("tileHeight") - world.engine.canvas.height + world.get("viewportBottom"));
if (min > 0) min = 0;
if (y < min) y = min;
}
world.set({x: x, y: y});
},
onDragEnd: function(e) {
var world = this.world;
world.startDragWorldX = undefined;
world.startDragWorldY = undefined;
},
onMouseMove: function(e) {
var mx = this.mx = e.pageX - this.world.get("x") - this.engine.canvas.offsetLeft + this.engine.canvas.scrollLeft,
my = this.my = e.pageY - this.world.get("y") - this.engine.canvas.offsetTop + this.engine.canvas.scrollTop,
id = this.world.getWorldIndex({x: mx, y: my}),
sprites = this.world.filterAt(mx, my),
nameOrIds = _.map(sprites, function(sprite) {
if (sprite.get("type") == "tile") return sprite.get("name")
return sprite.get("id");
});
if (this.debugPanel)
this.debugPanel.set({sprites: nameOrIds, mx: mx, my: my});
}
});
}).call(this);

713
games/mario/src/hero.js Normal file
View File

@@ -0,0 +1,713 @@
(function() {
// Velocity and acceleration values in absolute values
var walkVelocity = 160,
walkMinVelocity = 60,
walkAcceleration = 150,
runVelocity = 220,
runMinVelocity = 100,
runAcceleration = 400,
releaseDeceleration = 200,
skidDeceleration = 400,
jumpVelocity = 650,
jumpDeceleration = 1400,
jumpHoldDeceleration = 900,
fallAcceleration = 1200,
airTurnaroundDeceleration = 400,
fallVelocity = 600,
idleDelay = 2500,
walkDelay = 100,
koDelay = 100,
runDelay = 50,
attackDelay = 50,
attackSequences = [37, 38, 39, 38],
jumpAttackSequences = [41],
skidAttackSequences = [25],
walkSequences = [22, 23, 24, 23],
runSequences = [22, 23, 24, 23],
hurtDelay = 300,
hurtBounceVelocity = -300,
hurtSequences = [27];
var animations = {
"idle-left": {
sequences: [21],
delay: idleDelay,
velocity: 0,
acceleration: 0,
scaleX: -1,
scaleY: 1
},
"idle-right": {
sequences: [21],
delay: idleDelay,
velocity: 0,
acceleration: 0,
scaleX: 1,
scaleY: 1
},
"walk-left": {
sequences: walkSequences,
delay: walkDelay,
velocity: -walkVelocity,
minVelocity: -walkMinVelocity,
acceleration: walkAcceleration,
scaleX: -1,
scaleY: 1
},
"walk-right": {
sequences: walkSequences,
delay: walkDelay,
velocity: walkVelocity,
minVelocity: walkMinVelocity,
acceleration: walkAcceleration,
scaleX: 1,
scaleY: 1
},
"run-left": {
sequences: runSequences,
delay: runDelay,
velocity: -runVelocity,
minVelocity: -runMinVelocity,
acceleration: runAcceleration,
scaleX: -1,
scaleY: 1
},
"run-right": {
sequences: runSequences,
delay: runDelay,
velocity: runVelocity,
minVelocity: runMinVelocity,
acceleration: runAcceleration,
scaleX: 1,
scaleY: 1
},
"release-left": {
sequences: walkSequences,
delay: walkDelay,
velocity: 0,
acceleration: releaseDeceleration,
scaleX: -1,
scaleY: 1
},
"release-right": {
sequences: walkSequences,
delay: walkDelay,
velocity: 0,
acceleration: releaseDeceleration,
scaleX: 1,
scaleY: 1
},
"skid-left": {
sequences: [25],
velocity: 0,
acceleration: skidDeceleration,
scaleX: 1,
scaleY: 1
},
"skid-right": {
sequences: [25],
velocity: 0,
acceleration: skidDeceleration,
scaleX: -1,
scaleY: 1
},
"jump-left": {
sequences: [26],
delay: 1000,
velocity: -walkVelocity,
acceleration: airTurnaroundDeceleration,
yStartVelocity: -jumpVelocity,
yEndVelocity: fallVelocity,
yAscentAcceleration: jumpDeceleration,
yHoldAscentAcceleration: jumpHoldDeceleration,
yDescentAcceleration: fallAcceleration,
scaleX: -1,
scaleY: 1
},
"jump-right": {
sequences: [26],
delay: 1000,
velocity: walkVelocity,
acceleration: airTurnaroundDeceleration,
yStartVelocity: -jumpVelocity,
yEndVelocity: fallVelocity,
yAscentAcceleration: jumpDeceleration,
yHoldAscentAcceleration: jumpHoldDeceleration,
yDescentAcceleration: fallAcceleration,
scaleX: 1,
scaleY: 1
},
"ko-left": {
sequences: [27],
delay: koDelay,
velocity: 0,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration,
scaleX: -1,
scaleY: 1
},
"ko-right": {
sequences: [27],
delay: koDelay,
velocity: 0,
yVelocity: fallVelocity,
yAcceleration: fallAcceleration,
scaleX: 1,
scaleY: 1
},
"dead-left": {
sequences: [27],
velocity: 0,
scaleX: -1,
scaleY: 1
},
"dead-right": {
sequences: [27],
velocity: 0,
scaleX: 1,
scaleY: 1
}
};
animations["idle-attack-left"] = _.extend({}, animations["idle-left"], {sequences: attackSequences, delay: attackDelay});
animations["idle-attack-right"] = _.extend({}, animations["idle-right"], {sequences: attackSequences, delay: attackDelay});
animations["walk-attack-left"] = _.extend({}, animations["walk-left"], {sequences: attackSequences, delay: attackDelay});
animations["walk-attack-right"] = _.extend({}, animations["walk-right"], {sequences: attackSequences, delay: attackDelay});
animations["run-attack-left"] = _.extend({}, animations["run-left"], {sequences: attackSequences, delay: attackDelay});
animations["run-attack-right"] = _.extend({}, animations["run-right"], {sequences: attackSequences, delay: attackDelay});
animations["release-attack-left"] = _.extend({}, animations["release-left"], {sequences: attackSequences, delay: attackDelay});
animations["release-attack-right"] = _.extend({}, animations["release-right"], {sequences: attackSequences, delay: attackDelay});
animations["jump-attack-left"] = _.extend({}, animations["jump-left"], {sequences: jumpAttackSequences, delay: attackDelay});
animations["jump-attack-right"] = _.extend({}, animations["jump-right"], {sequences: jumpAttackSequences, delay: attackDelay});
animations["skid-attack-left"] = _.extend({}, animations["skid-left"], {sequences: skidAttackSequences, delay: attackDelay});
animations["skid-attack-right"] = _.extend({}, animations["skid-right"], {sequences: skidAttackSequences, delay: attackDelay});
var hurtAnimation = {sequences: hurtSequences, delay: hurtDelay};
animations["idle-hurt-left"] = _.extend({}, animations["idle-left"], hurtAnimation);
animations["idle-hurt-right"] = _.extend({}, animations["idle-right"], hurtAnimation);
animations["walk-hurt-left"] = _.extend({}, animations["walk-left"], hurtAnimation);
animations["walk-hurt-right"] = _.extend({}, animations["walk-right"], hurtAnimation);
animations["run-hurt-left"] = _.extend({}, animations["run-left"], hurtAnimation);
animations["run-hurt-right"] = _.extend({}, animations["run-right"], hurtAnimation);
animations["release-hurt-left"] = _.extend({}, animations["release-left"], hurtAnimation);
animations["release-hurt-right"] = _.extend({}, animations["release-right"], hurtAnimation);
animations["jump-hurt-left"] = _.extend({}, animations["jump-left"], hurtAnimation);
animations["jump-hurt-right"] = _.extend({}, animations["jump-right"], hurtAnimation);
animations["skid-hurt-left"] = _.extend({}, animations["skid-left"], hurtAnimation);
animations["skid-hurt-right"] = _.extend({}, animations["skid-right"], hurtAnimation);
Backbone.Hero = Backbone.Character.extend({
defaults: _.extend({}, Backbone.Character.prototype.defaults, {
name: "hero",
type: "character",
hero: true,
spriteSheet: undefined,
width: 32,
height: 64,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 32,
paddingBottom: 0,
state: "idle-right",
velocity: 0,
acceleration: 0,
yVelocity: 0,
yAcceleration: 0,
collision: true,
dead: false,
health: 1,
healthMax: 2,
attackDamage: 1,
coins: 0,
ignoreInput: false,
canAttack: false,
canTurnInJump: false
}),
animations: animations,
saveAttributes: _.union(
Backbone.Character.prototype.saveAttributes,
["nextState", "velocity", "acceleration", "yVelocity", "yAcceleration"]
),
initialize: function(attributes, options) {
options || (options = {});
Backbone.Character.prototype.initialize.apply(this, arguments);
this.input = options.input;
},
onAttach: function() {
if (this.input) {
this.stopListening(this.input);
this.listenTo(this.input, "change:right", _.partial(this.dirToggled, "right"));
this.listenTo(this.input, "change:left", _.partial(this.dirToggled, "left"));
this.listenTo(this.input, "change:buttonB", this.buttonBToggled);
this.listenTo(this.input, "change:buttonA", this.buttonAToggled);
}
},
onDetach: function() {
if (this.input) this.stopListening(this.input);
},
toggleDirection: function(dirIntent) {
return this.dirToggled(dirIntent);
},
ignoreInput: function() {
if (this.get("ignoreInput") || this.get("dead")) return true;
var cur = this.getStateInfo();
if (cur.mov == "ko" || cur.mov == "dead" || cur.mov2 == "hurt") return true;
return false;
},
// User input toggled in right or left direction.
// Can be pressed or depressed
dirToggled: function(dirIntent) {
if (this.ignoreInput()) return this;
if (dirIntent != "left" && dirIntent != "right")
throw "Invalid or missing dirIntent. Must be left or right."
var cur = this.getStateInfo(),
now = _.now(),
opoIntent = dirIntent == "right" ? "left" : "right",
dirPressed = this.input ? this.input[dirIntent+"Pressed"]() : false,
opoPressed = this.input ? this.input[opoIntent+"Pressed"]() : false,
run = this.input ? this.input.buttonBPressed() : false,
velocity = this.get("velocity"),
attrs = {};
if (dirPressed) {
// Pressed. Intent to move in that direction
if (cur.mov == "jump") {
if (this.get("canTurnInJump"))
attrs.state = this.buildState("jump", cur.mov2, dirIntent);
if (dirIntent != cur.dir && velocity)
attrs.nextState = this.buildState("skid", cur.mov2, opoIntent);
else
attrs.nextState = this.buildState(run ? "run" : "walk", cur.mov2, dirIntent);
} else if (cur.dir == dirIntent || cur.mov == "idle") {
// Start walking or running
attrs.state = this.buildState(run ? "run" : "walk", cur.mov2, dirIntent);
var animation = this.getAnimation(attrs.state);
if (animation.minVelocity && Math.abs(velocity) < Math.abs(animation.minVelocity))
attrs.velocity = animation.minVelocity;
this.startWalk = now;
} else if (cur.dir == opoIntent) {
// Skid trying to stop before turning
attrs.state = this.buildState("skid", cur.mov2, opoIntent);
attrs.nextState = this.buildState(run ? "run" : "walk", cur.mov2, dirIntent);
}
} else if (opoPressed) {
// Depressed but opposite direction still pressed. Intent = turnaround.
// Handle by calling the opposite direction press event.
this.dirToggled(opoIntent);
} else {
// Depressed. Intent = stop to idle
if (cur.mov == "jump") {
attrs.nextState = this.buildState("release", cur.mov2, dirIntent);
} else {
attrs.state = this.buildState("release", cur.mov2, dirIntent);
attrs.nextState = this.buildState("idle", cur.mov2, dirIntent);
if (now < this.startWalk + 250)
attrs.velocity = velocity = velocity * 0.5;
}
}
if (!_.isEmpty(attrs)) this.set(attrs);
return this;
},
// Attack and run
buttonBToggled: function() {
if (this.ignoreInput()) return this;
var cur = this.getStateInfo(),
pressed = this.input ? this.input.buttonBPressed() : false;
if (pressed && cur.mov == "walk") {
cur.mov = "run";
this.set("state", this.buildState(cur.mov, cur.mov2, cur.dir));
this.cancelUpdate = true;
} else if (!pressed && cur.mov == "run") {
cur.mov = "walk";
this.set("state", this.buildState(cur.mov, cur.mov2, cur.dir));
this.cancelUpdate = true;
}
if (!this.get("canAttack")) return this;
if (pressed && cur.mov2 != "attack") {
var nextState = this.get("nextState");
if (nextState) {
var nex = this.getStateInfo(nextState);
nextState = this.buildState(nex.mov, "attack", nex.dir);
}
this.startNewAnimation(this.buildState(cur.mov, "attack", cur.dir), {nextState: nextState}, this.endAttack);
this.cancelUpdate = true;
} else if (!pressed && cur.mov2 == "attack") {
this.endAttack();
}
return this;
},
endAttack: function() {
var cur = this.getStateInfo(),
attrs = {state: this.buildState(cur.mov, cur.dir)},
nextState = this.get("nextState"),
nex = nextState ? this.getStateInfo(nextState) : null;
if (nextState) attrs.nextState = this.buildState(nex.mov, nex.dir);
this.whenAnimationEnds = null;
this.set(attrs);
return this;
},
knockout: function(sprite, dir) {
dir || (dir = cur.dir);
var cur = this.getStateInfo(),
opo = dir == "left" ? "right" : "left",
state = this.buildState("ko", opo);
this.set({
state: state,
velocity: this.animations[state].velocity,
yVelocity: -this.animations[state].yVelocity,
nextState: this.buildState("dead", null, opo),
dead: true,
collision: false
});
this.cancelUpdate = true;
return this;
},
hurt: function(sprite, dir) {
this.set({
state: this.buildState("jump", "hurt", dir == "left" ? "right" : "left"),
nextState: this.buildState("idle", null, dir),
yVelocity: hurtBounceVelocity,
velocity: hurtBounceVelocity * (dir == "left" ? -1 : 1) / 2,
sequenceIndex: 0
});
return this;
},
isAttacking: function() {
return this.attributes.state.indexOf("-attack") > 0;
},
hit: function(sprite, dir, dir2) {
if (this._handlingSpriteHit) return this;
this._handlingSpriteHit = sprite;
var cur = this.getStateInfo(),
type = sprite.get("type");
if (type == "artifact") {
switch (sprite.get("name")) {
case "a-coin":
this.cancelUpdate = true;
this.set("coins", this.get("coins") + 1);
break;
}
} else if (type == "character" && cur.mov2 != "hurt") {
if (this.isAttacking() && cur.dir == dir) {
sprite.trigger("hit", this, cur.opo);
} else {
if (sprite.isAttacking()) {
this.cancelUpdate = true;
var attackDamage = sprite.get("attackDamage") || 1;
this.set({health: Math.max(this.get("health") - attackDamage, 0)}, {sprite: sprite, dir: dir, dir2: dir2});
}
}
}
this._handlingSpriteHit = undefined;
return this;
},
// Jump
buttonAToggled: function() {
if (this.ignoreInput()) return this;
var state = this.get("state"),
cur = this.getStateInfo(),
attrs = {};
if (this.input && this.input.buttonAPressed() && cur.mov != "jump") {
// Set new state (keep old as next)
attrs.state = this.buildState("jump", cur.mov2, cur.dir);
attrs.nextState = state;
// Determine vertical velocity as a factor of horizontal velocity
var jumpAnimation = this.getAnimation(attrs.state),
velocity = this.get("velocity"),
walkVelocity = this.getAnimation("walk-right").velocity,
runVelocity = this.getAnimation("run-right").velocity,
ratio = Math.abs((Math.abs(velocity) > walkVelocity ? velocity : walkVelocity) / runVelocity);
attrs.yVelocity = Math.round(jumpAnimation.yStartVelocity * (ratio + (1-ratio)/2));
var heroWidth = this.get("width"),
tileHeight = this.get("height"),
heroHeight = tileHeight - this.get("paddingTop") - this.get("paddingBottom"),
heroBottomY = Math.round(this.get("y") - 4) + tileHeight - this.get("paddingBottom"),
heroTopY = heroBottomY - heroHeight,
heroLeftX = this.get("x"),
topLeftTile = heroTopY > 0 ? this.world.findAt(heroLeftX + heroWidth*0.4, heroTopY, "tile", this, true) : null,
topRightTile = heroTopY > 0 ? this.world.findAt(heroLeftX + heroWidth*0.6, heroTopY, "tile", this, true) : null;
if (topLeftTile || topRightTile) attrs.yVelocity = -2*60;
// Keep the horizontal velocity
jumpAnimation.minY = (this.get("y") - this.world.height()) * ratio;
}
if (!_.isEmpty(attrs)) this.set(attrs);
return this;
},
sequenceDelay: function(animation) {
var velocity = this.get("velocity");
return animation.velocity && velocity ?
animation.delay * animation.velocity / velocity :
animation.delay;
},
update: function(dt) {
// Movements are only possible inside a world
if (!this.world) return true;
this.cancelUpdate = false;
// Velocity and state
var hero = this,
dead = this.get("dead"),
input = !this.get("ignoreInput") ? this.input : null,
velocity = this.get("velocity") || 0,
yVelocity = this.get("yVelocity") || 0,
yAcceleration = null,
x = this.get("x"),
y = this.get("y"),
state = this.get("state"),
cur = this.getStateInfo(),
animation = this.getAnimation(),
nextState = this.get("nextState"),
nex = this.getStateInfo(nextState),
nextAnimation = nextState ? (this.getAnimation(nextState) || {}) : null,
attrs = {};
attrs.sequenceIndex = this.updateSequenceIndex();
switch (cur.mov + "-" + cur.dir) {
case "walk-right":
case "run-right":
case "release-left":
case "skid-left":
if (velocity < animation.velocity)
velocity += Math.round(animation.acceleration * (dt/1000));
if (velocity >= animation.velocity) {
velocity = animation.velocity;
if (nextState) {
attrs.state = nextState;
animation = nextAnimation;
if (animation.minVelocity && Math.abs(velocity) < Math.abs(animation.minVelocity))
velocity = animation.minVelocity;
attrs.nextState = null;
}
}
attrs.velocity = velocity;
break;
case "walk-left":
case "run-left":
case "release-right":
case "skid-right":
if (velocity > animation.velocity)
velocity -= Math.round(animation.acceleration * (dt/1000));
if (velocity <= animation.velocity) {
velocity = animation.velocity;
if (nextState) {
attrs.state = nextState;
animation = nextAnimation;
if (animation.minVelocity && Math.abs(velocity) < Math.abs(animation.minVelocity))
velocity = animation.minVelocity;
attrs.nextState = null;
}
}
attrs.velocity = velocity;
break;
}
switch (cur.mov) {
case "idle":
// TO DO: This should never happen - but seems to. Figure out why...
if (velocity != 0) {
if (input && input.rightPressed())
this.toggleDirection("right");
else if (input && input.leftPressed())
this.toggleDirection("left");
else
attrs.velocity = velocity = 0;
}
break;
case "jump":
// Update vertical velocity. Determine proper vertical acceleration.
if (yVelocity < animation.yEndVelocity) {
yAcceleration = yVelocity < 0 ? animation.yAscentAcceleration : animation.yDescentAcceleration;
if (yVelocity < 0 && input && input.buttonAPressed() && y > animation.minY)
yAcceleration = animation.yHoldAscentAcceleration;
yVelocity += yAcceleration * (dt/1000);
}
if (yVelocity >= animation.yEndVelocity)
yVelocity = animation.yEndVelocity;
attrs.yVelocity = yVelocity;
// Update horizontal velocity if trying to turnaround
if (input && input.leftPressed() && velocity > -Math.abs(animation.velocity)) {
velocity -= Math.abs(animation.acceleration) * (dt/1000);
attrs.velocity = velocity;
} else if (input && input.rightPressed() && velocity < Math.abs(animation.velocity)) {
velocity += Math.abs(animation.acceleration) * (dt/1000);
attrs.velocity = velocity;
}
break;
case "ko":
if (yVelocity < animation.yVelocity)
yVelocity += animation.yAcceleration * (dt/1000);
if (yVelocity >= animation.yVelocity)
yVelocity = animation.yVelocity;
attrs.yVelocity = yVelocity;
break;
}
// Collision detection
var collision = this.get("collision"),
tileWidth = this.get("width"),
tileHeight = this.get("height"),
paddingLeft = this.get("paddingLeft"),
paddingRight = this.get("paddingRight"),
paddingBottom = this.get("paddingBottom"),
paddingTop = this.get("paddingTop"),
heroWidth = tileWidth - paddingLeft - paddingRight,
heroHeight = tileHeight - paddingTop - paddingBottom,
heroLeftX = Math.round(x + velocity * (dt/1000)) + paddingLeft,
heroRightX = heroLeftX + heroWidth,
relativeVelocity = 0;
var heroBottomY, heroTopY,
bottomPlatform, sprite, i;
function updateTopBottom() {
heroBottomY = Math.round(y + yVelocity * (dt/1000)) + tileHeight - paddingBottom;
heroTopY = heroBottomY - heroHeight;
hero.buildCollisionMap(heroTopY, heroRightX, heroBottomY, heroLeftX);
if (collision)
hero.world.findCollisions(hero.collisionMap, null, hero, true);
}
updateTopBottom();
if (yVelocity >= 0) {
// Standing or falling, implement gravity
var bottomWorld = this.world.height() + tileHeight,
floor = this.get("floor") || bottomWorld,
bottomY = Math.min(floor, bottomWorld);
for (i = 0; i < this.collisionMap.bottom.sprites.length; i++) {
sprite = this.collisionMap.bottom.sprites[i];
bottomY = Math.min(bottomY, sprite.getTop(true));
if (sprite.get("type") == "platform") bottomPlatform = sprite;
}
if (cur.mov == "jump" && cur.mov2 == null) attrs.sequenceIndex = 1;
if (heroBottomY >= bottomWorld) {
attrs.y = y = bottomY - tileHeight + paddingBottom;
attrs.velocity = velocity = 0;
attrs.yVelocity = yVelocity = 0;
attrs.state = this.buildState("dead", cur.dir);
attrs.dead = true;
}
function land(bottomY) {
attrs.yVelocity = yVelocity = 0;
attrs.y = y = bottomY - tileHeight + paddingBottom;
updateTopBottom();
attrs.state = nextState;
if (nex.move == "walk" || nex.move == "run")
attrs.nextState = hero.buildState(input && input.buttonBPressed() ? "run" : "walk", cur.mov2, nex.dir);
else if (nex.mov == "skid")
attrs.nextState = hero.buildState(input && input.buttonBPressed() ? "run" : "walk", cur.mov2, nex.opo);
else if(nex.mov == "release")
attrs.nextState = hero.buildState("idle", cur.mov2, nex.dir);
else if (nex.mov == "dead") {
attrs.velocity = velocity = 0;
}
}
if (yVelocity > 0 && heroBottomY >= bottomY) {
// Stop falling
land(bottomY);
for (i = 0; i < this.collisionMap.bottom.sprites.length; i++)
this.collisionMap.bottom.sprites[i].trigger("hit", this, "top", cur.dir);
if (this.cancelUpdate) return true;
} else if (cur.mov != "jump" && yVelocity == 0 && heroBottomY < bottomY) {
// Start falling if no obstacle below
attrs.nextState = state;
attrs.state = this.buildState("jump", cur.mov2, cur.dir);
} else if (yVelocity == 0 && heroBottomY == bottomY) {
if (bottomPlatform)
relativeVelocity = bottomPlatform.get("velocity");
}
} else {
// Velocity is negative (going up). Stop if obstacle above.
var topY = Math.max(-400, this.get("ceiling") || -400);
for (i = 0; i < this.collisionMap.top.sprites.length; i++)
if (!dead && heroTopY > 0 )
topY = Math.max(topY, this.collisionMap.top.sprites[i].getBottom(true));
if (cur.mov == "jump" && cur.mov2 == null) attrs.sequenceIndex = 0;
if (heroTopY < topY) {
attrs.yVelocity = yVelocity = 0;
attrs.y = y = topY - paddingTop;
updateTopBottom();
for (i = 0; i < this.collisionMap.top.sprites.length; i++)
this.collisionMap.top.sprites[i].trigger("hit", this, "bottom", cur.dir);
if (this.cancelUpdate) return true;
}
}
if (velocity <= 0) {
// Stop if obstacle left
var leftX = 0;
for (i = 0; i < this.collisionMap.left.sprites.length; i++)
if (heroTopY > 0 )
leftX = Math.max(leftX, this.collisionMap.left.sprites[i].getRight(true));
if (heroLeftX <= leftX) {
attrs.velocity = velocity = 0;
attrs.x = x = leftX - paddingLeft;
for (i = 0; i < this.collisionMap.left.sprites.length; i++)
this.collisionMap.left.sprites[i].trigger("hit", this, "right", cur.mov2);
if (this.cancelUpdate) return true;
}
}
if (velocity >= 0) {
// Stop if obstacle to the right
var rightX = this.world.width();
for (i = 0; i < this.collisionMap.right.sprites.length; i++)
if (heroTopY > 0 )
rightX = Math.min(rightX, this.collisionMap.right.sprites[i].getLeft(true));
if (heroRightX >= rightX) {
attrs.velocity = velocity = 0;
attrs.x = x = rightX - heroWidth - paddingLeft;
for (i = 0; i < this.collisionMap.right.sprites.length; i++)
this.collisionMap.right.sprites[i].trigger("hit", this, "left", cur.mov2);
if (this.cancelUpdate) return true;
}
}
if (velocity || relativeVelocity) attrs.x = x = x + Math.round((velocity + relativeVelocity) * (dt/1000));
if (yVelocity) attrs.y = y = y + Math.round(yVelocity * (dt/1000));
// Set modified attributes
if (!_.isEmpty(attrs)) this.set(attrs);
if (this.debugPanel)
this.debugPanel.set({velocity: velocity});
return true;
}
});
}).call(this);

476
games/mario/src/input.js Normal file
View File

@@ -0,0 +1,476 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var keyboardImg = new Image();
keyboardImg.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAAAwCAYAAABHTnUeAAACYklEQVR42u3dS5KCQBAEUNx5LJbcYs42t2DJsViOizFCnQH6U9Wd2ZW5JuQVmtAoEd4mRQmcW2+AovSMCqCEjgqghM5hAbZt++qNe808z9852zP7me1s/n8LgDZAyiCj+JntjP4/BUAd4GqQEfzMdla/CgDkZ7az+lUAID+zndWvAgD5me2sfhUAyM9sZ/WrAEB+ZjurXwUA8jPbWf0qAJCf2c7qVwGA/Mx2Vr8KAORntrP6VQAgf6798VpFhsd+zO0W/itX7vYpfvMCPJGlB7lkCEu/d1SAc/+RLWfbHL9pAV6RoxXAqtgqwPUMnz6LD/+R36QAVsDSIWr9uTPWzNb6HsDyvfE69mdGb39VAc7OQCMXoGbGlgWwPjF5Hvuj9X3tuv/KX1SAlEtvhAKUzNqqAB5X5ZYFONmHqT+rAKVrztSkDIdYACu/ld1rSep97K+OsUeBQxag5xzeBfC8H2tx8jl6b7wK7LIEut/v07qu1eDUIaz9tUEqgOVSVAVIxFqBU4co9edkWZZp3/fTbXovgby/jQu/BMqBj1YA9JvgFu9B+JvgnCEiFADla9BWJyB9DZqQ51Jh5AIg/RA2wtWX9oewHulZAIube4RHIX73ZWov8R/NQPcoRMvoYbi31yp2IBRguIfhWkQFeHutYgdiAYZ4HNo7KgCmndWvAgD5me2sfhUAyM9sZ/WrAEB+ZjurXwUA8jPbWf0qAJCf2c7qVwGA/Mx2Vr8KAORntrP6VQAgP7Od1a8CAPmZ7ax+/UkemJ/ZzujX36QC+pntbH79UbYSOiqAEjoqgBI6KoASOj+RrUxeKnOx4gAAAABJRU5ErkJggg==";
Backbone.InputButton = Backbone.Element.extend({
defaults: _.extend({}, Backbone.Element.prototype.defaults, {
// Relative position references
left: undefined,
right: undefined,
top: undefined,
bottom: undefined,
backgroundColor: "transparent",
pressed: false
}),
onAttach: function() {
Backbone.Element.prototype.onAttach.apply(this, arguments);
this.stopListening();
this.calculatePosition();
},
onDetach: function() {
Backbone.Element.prototype.onDetach.apply(this, arguments);
this.set("pressed", false);
},
calculatePosition: function() {
// Set x and y based on relative position references
if (!this.engine) return this;
var canvas = this.engine.canvas,
attrs = {};
if (this.attributes.left !== undefined)
attrs.x = this.attributes.left;
else if (this.attributes.right !== undefined)
attrs.x = canvas.width - this.attributes.right - this.attributes.width;
else
throw "InputButton " + this.id + " missing left or right attributes.";
if (this.attributes.top !== undefined)
attrs.y = this.attributes.top;
else if (this.attributes.bottom !== undefined)
attrs.y = canvas.height - this.attributes.bottom - this.attributes.height;
else
throw "InputButton " + this.id + " missing top or bottom attributes.";
this.set(attrs);
return this;
}
});
Backbone.LeftInputButton = Backbone.InputButton.extend({
onDraw: function(context) {
var x = this.get("x"),
y = this.get("y"),
width = this.get("width"),
height = this.get("height"),
pressed = this.get("pressed");
//drawRect(context, x, y, width, height, "rgba(64, 64, 64, 0.5)");
context.save();
context.beginPath();
context.moveTo(x+130, y+20);
context.lineTo(x+40, y+80);
context.lineTo(x+130, y+140);
context.lineTo(x+100, y+80);
context.lineTo(x+130, y+20);
context.lineJoin = 'bevel';
context.fillStyle = pressed ? "#00FF00" : "#009900";
context.fill();
context.lineWidth = 5;
context.strokeStyle = '#111';
context.stroke();
context.restore();
return this;
}
});
Backbone.RightInputButton = Backbone.InputButton.extend({
onDraw: function(context) {
var x = this.get("x"),
y = this.get("y"),
width = this.get("width"),
height = this.get("height"),
pressed = this.get("pressed");
//drawRect(context, x, y, width, height, "rgba(128, 128, 128, 0.5)");
context.save();
context.beginPath();
context.moveTo(x+30, y+20);
context.lineTo(x+120, y+80);
context.lineTo(x+30, y+140);
context.lineTo(x+60, y+80);
context.lineTo(x+30, y+20);
context.lineJoin = 'bevel';
context.fillStyle = pressed ? "#00FF00" : "#009900";
context.fill();
context.lineWidth = 5;
context.strokeStyle = '#111';
context.stroke();
context.restore();
return this;
}
});
Backbone.AInputButton = Backbone.InputButton.extend({
onDraw: function(context) {
var x = this.get("x"),
y = this.get("y"),
width = this.get("width"),
height = this.get("height"),
pressed = this.get("pressed");
//drawRect(context, x, y, width, height, "rgba(255, 255, 255, 0.5)");
context.save();
context.beginPath();
context.arc(x+80, y+80, 60, 0, 2*Math.PI, false);
context.fillStyle = "#111";
context.fill();
context.beginPath();
context.arc(x+80, y+80, 55, 0, 2*Math.PI, false);
context.fillStyle = pressed ? "#0000FF" : "#000099";
context.fill();
context.restore();
return this;
}
});
Backbone.BInputButton = Backbone.InputButton.extend({
onDraw: function(context) {
var x = this.get("x"),
y = this.get("y"),
width = this.get("width"),
height = this.get("height"),
pressed = this.get("pressed");
//drawRect(context, x, y, width, height, "rgba(192, 192, 192, 0.5)");
context.save();
context.beginPath();
context.arc(x+80, y+80, 60, 0, 2*Math.PI, false);
context.fillStyle = "#111";
context.fill();
context.beginPath();
context.arc(x+80, y+80, 55, 0, 2*Math.PI, false);
context.fillStyle = pressed ? "#FF0000" : "#990000";
context.fill();
context.restore();
return this;
}
});
// Input class; a Backbone Model which captures input events
// and stores them as model attributes with true if pressed.
// Supports keyboard, and a drawn touchpad activated by touch
// or mouse events.
Backbone.Input = Backbone.Model.extend({
defaults: {
// Supported buttons
left: false, // Left button pressed?
right: false, // Right button pressed?
buttonA: false, // A button pressed? (X on keyboard)
buttonB: false, // B button pressed? (Z on keyboard)
// Touch pad
drawTouchpad: "auto", // Boolean to draw. Set to auto to draw only for touch devices.
touchEnabled: false // Touch device? Automatically determined. Do not set.
},
buttons: [{
id: "left", name: "left-input-button",
left: 0, bottom: 0, width: 160, height: 160
}, {
id: "right", name: "right-input-button",
left: 160, bottom: 0, width: 160, height: 160
}, {
id: "buttonA", name: "a-input-button",
right: 0, bottom: 0, width: 160, height: 160
}, {
id: "buttonB", name: "b-input-button",
right: 160, bottom: 0, width: 160, height: 160
}],
initialize: function(attributes, options) {
options || (options = {});
this._ongoingTouches = [];
_.bindAll(this, "rightPressed", "leftPressed", "buttonBPressed", "buttonAPressed");
for (var i = 0; i < this.buttons.length; i++) {
var config = this.buttons[i],
cls = _.classify(config.name);
config.instance = new Backbone[cls](config);
}
// Handle touch events
var touchEnabled =
window.forceTouchpad ||
"ontouchstart" in window ||
"onorientationchange" in window ||
window.navigator.msMaxTouchPoints ||
window.navigator.maxTouchPoints > 0 ||
window.navigator.isCocoonJS;
this.set({touchEnabled: touchEnabled});
// Debug panel
var debugPanel = this.debugPanel = options.debugPanel;
if (debugPanel) {
this.on("change:pressed", function() {
debugPanel.set({pressed: this.get("pressed")});
});
this.on("change:touched", function() {
debugPanel.set({touched: this.get("touched")});
});
this.on("change:clicked", function() {
debugPanel.set({clicked: this.get("clicked")});
});
}
if (touchEnabled) {
// Prevent touch scroll
$(document).bind("touchmove.InputTouchScroll", function(e) {
e.preventDefault();
e.stopPropagation();
return false;
});
// Prevent links from opening popup after a while
document.documentElement.style.webkitTouchCallout = "none";
}
this.on("attach", this.onAttach, this);
this.on("detach", this.onDetach, this);
},
onAttach: function() {
this.onDetach();
// Handle keyboard input
$(document).on("keydown.Input", this.onKeydown.bind(this));
$(document).on("keyup.Input", this.onKeyup.bind(this));
if (this.hasTouchpad()) {
if (this.get("touchEnabled")) {
if (window.navigator.msMaxTouchPoints) {
$(document).on("pointerdown.InputTouchpad", this.onTouchStart.bind(this));
$(document).on("pointermove.InputTouchpad", this.onTouchMove.bind(this));
$(document).on("pointerup.InputTouchpad", this.onTouchEnd.bind(this));
$(document).on("pointercancel.InputTouchpad", this.onTouchEnd.bind(this));
} else {
$(document).on("touchstart.InputTouchpad", this.onTouchStart.bind(this));
$(document).on("touchmove.InputTouchpad", this.onTouchMove.bind(this));
$(document).on("touchend.InputTouchpad", this.onTouchEnd.bind(this));
$(document).on("touchleave.InputTouchpad", this.onTouchEnd.bind(this));
$(document).on("touchcancel.InputTouchpad", this.onTouchEnd.bind(this));
}
} else {
// Fallback to handling mouse events
$(document).on("mousedown.InputTouchpad", this.onMouseDown.bind(this));
$(document).on("mousemove.InputTouchpad", this.onMouseDown.bind(this));
$(document).on("mouseup.InputTouchpad", this.onMouseUp.bind(this));
}
for (var i = 0; i < this.buttons.length; i++) {
this.buttons[i].instance.engine = this.engine;
this.buttons[i].instance.trigger("attach");
}
}
},
onDetach: function() {
$(document).off(".Input");
this._ongoingTouches = [];
this.set({
left: false,
right: false,
buttonA: false,
buttonB: false
});
if (this.hasTouchpad()) {
$(document).off(".InputTouchpad");
for (var i = 0; i < this.buttons.length; i++) {
this.buttons[i].instance.trigger("detach");
this.buttons[i].instance.engine = undefined;
}
}
},
hasTouchpad: function() {
var drawTouchpad = this.get("drawTouchpad");
if (_.isBoolean(drawTouchpad)) return drawTouchpad;
if (drawTouchpad == "auto" && this.get("touchEnabled")) return true;
return false;
},
// Engine core functions
update: function(dt) {
return true;
},
draw: function(context, options) {
if (this.hasTouchpad()) {
for (var i = 0; i < this.buttons.length; i++)
this.buttons[i].instance.draw(context);
} else{
context.drawImage(keyboardImg, 8, context.canvas.height - 56);
}
return this;
},
// Keyboard events
onKeydown: function(e) {
var buttonId = this.keyCodeToButtonId(e.keyCode),
attrs = {};
attrs[e.keyCode] = true;
if (buttonId) {
attrs[buttonId] = true;
if (this.hasTouchpad())
for (var i = 0; i < this.buttons.length; i++)
if (this.buttons[i].instance.id == buttonId)
this.buttons[i].instance.set("pressed", true);
}
this.set(attrs);
},
onKeyup: function(e) {
var buttonId = this.keyCodeToButtonId(e.keyCode),
attrs = {};
attrs[e.keyCode] = false;
if (buttonId) {
attrs[buttonId] = false;
if (this.hasTouchpad())
for (var i = 0; i < this.buttons.length; i++)
if (this.buttons[i].instance.id == buttonId)
this.buttons[i].instance.set("pressed", false);
}
this.set(attrs);
},
// Touch events
detectTouched: function() {
var attrs = {};
var canvas = this.engine.canvas;
// 计算 CSS 缩放比例
var scaleX = canvas.width / canvas.offsetWidth;
var scaleY = canvas.height / canvas.offsetHeight;
for (var i = 0; i < this.buttons.length; i++) {
var button = this.buttons[i].instance,
touched = false;
for (var t = 0; t < this._ongoingTouches.length; t++) {
// 转换触摸坐标到 canvas 坐标系
var touchX = (this._ongoingTouches[t].pageX - canvas.offsetLeft) * scaleX;
var touchY = (this._ongoingTouches[t].pageY - canvas.offsetTop) * scaleY;
touched = button.overlaps(touchX, touchY);
if (touched) break;
}
attrs[button.id] = touched;
button.set("pressed", touched);
}
this.set(attrs);
return this;
},
onTouchStart: function(e) {
var touches = e.changedTouches || [{
identifier: e.pointerId,
pageX: e.pageX,
pageY: e.pageY
}];
for (var i = 0; i < touches.length; i++)
this._ongoingTouches.push(this._copyTouch(touches[i]));
this.detectTouched();
},
onTouchMove: function(e) {
var touches = e.changedTouches || [{
identifier: e.pointerId,
pageX: e.pageX,
pageY: e.pageY
}];
for (var i = 0; i < touches.length; i++) {
var idx = this._ongoingTouchIndexById(touches[i].identifier);
if (idx >= 0) this._ongoingTouches.splice(idx, 1, this._copyTouch(touches[i]));
}
this.detectTouched();
},
onTouchEnd: function(e) {
var touches = e.changedTouches || [{
identifier: e.pointerId,
pageX: e.pageX,
pageY: e.pageY
}];
for (var i=0; i < touches.length; i++) {
var idx = this._ongoingTouchIndexById(touches[i].identifier);
if (idx >= 0) this._ongoingTouches.splice(idx, 1);
}
this.detectTouched();
},
// Mouse events
onMouseDown: function(e) {
if (!e.which) return;
var canvas = this.engine.canvas;
// 计算 CSS 缩放比例
var scaleX = canvas.width / canvas.offsetWidth;
var scaleY = canvas.height / canvas.offsetHeight;
var x = (e.pageX - canvas.offsetLeft) * scaleX,
y = (e.pageY - canvas.offsetTop) * scaleY,
attrs = {};
for (var i = 0; i < this.buttons.length; i++) {
var button = this.buttons[i].instance,
clicked = button.overlaps(x, y);
attrs[button.id] = clicked;
button.set("pressed", clicked);
}
this.set(attrs);
},
onMouseUp: function(e) {
for (var i = 0; i < this.buttons.length; i++) {
var button = this.buttons[i].instance;
if (this.get(button.id)) {
this.set(button.id, false);
button.set("pressed", false);
}
}
},
// Button helpers
rightPressed: function() {
return !!this.get("right");
},
leftPressed: function() {
return !!this.get("left");
},
buttonBPressed: function() {
return !!this.get("buttonB");
},
buttonAPressed: function() {
return !!this.get("buttonA");
},
keyCodeToButtonId: function(keyCode) {
switch (keyCode) {
case 39:
return "right";
case 37:
return "left";
case 90:
return "buttonB";
case 88:
return "buttonA";
}
return null;
},
// Touch event helpers.
// Source: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Touch_events
_copyTouch: function(touch) {
return {
identifier: touch.identifier,
pageX: touch.pageX,
pageY: touch.pageY
};
},
_ongoingTouchIndexById: function(idToFind) {
for (var i = 0; i < this._ongoingTouches.length; i++) {
var id = this._ongoingTouches[i].identifier;
if (id == idToFind) return i;
}
return -1;
}
});
}).call(this);

View File

@@ -0,0 +1,50 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
// Load this file to persist in local storage.
// It will replace Backbone.World's save and fetch methods with
// naive implementations.
Backbone.World.prototype.fetch = function(options) {
options || (options = {});
var data = localStorage.getItem(this.id);
if (data) {
console.log("===== LOADING LOCAL STORAGE ====");
this.set(JSON.parse(data));
}
if (_.isFunction(options.success)) options.success();
return this;
};
Backbone.World.prototype.save = function(attributes, options) {
options || (options = {});
this.set({
state: "play",
sprites: this.sprites.map(function(sprite) {
return sprite.toSave.apply(sprite);
}),
savedOn: new Date().toJSON()
}, {silent: true});
console.log("===== SAVING LOCAL STORAGE ====");
localStorage.setItem(this.id, JSON.stringify(this.toJSON()));
if (_.isFunction(options.success)) options.success();
return this;
};
}).call(this);

72
games/mario/src/shapes.js Normal file
View File

@@ -0,0 +1,72 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
function drawRoundRect(ctx, x, y, width, height, borderRadius, fill, stroke) {
if (typeof stroke == "undefined" ) stroke = true;
if (typeof borderRadius === "undefined") borderRadius = 5;
ctx.save();
ctx.beginPath();
ctx.moveTo(x + borderRadius, y);
ctx.lineTo(x + width - borderRadius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + borderRadius);
ctx.lineTo(x + width, y + height - borderRadius);
ctx.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height);
ctx.lineTo(x + borderRadius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - borderRadius);
ctx.lineTo(x, y + borderRadius);
ctx.quadraticCurveTo(x, y, x + borderRadius, y);
ctx.closePath();
if (stroke) {
ctx.strokeStyle = stroke;
ctx.stroke();
}
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
ctx.restore();
}
function drawRect(ctx, x, y, width, height, fill, stroke) {
ctx.save();
if (fill) {
ctx.fillStyle = fill;
ctx.fillRect(x, y, width, height);
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.strokeRect(x, y, width, height);
}
ctx.restore();
}
function drawCircle(ctx, x, y, borderRadius, fill, stroke) {
ctx.save();
ctx.beginPath();
ctx.arc(x, y, borderRadius, 0, 2*Math.PI, false);
if (stroke) {
ctx.strokeStyle = stroke;
ctx.stroke();
}
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
ctx.restore();
}
_.extend(window, {
drawRect: drawRect,
drawRoundRect: drawRoundRect,
drawCircle: drawCircle
});
}).call(this);

914
games/mario/src/world.js Normal file
View File

@@ -0,0 +1,914 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
// Backbone.World is Backbone model which contains a collection of sprites.
Backbone.World = Backbone.Model.extend({
defaults: {
name: "",
x: 0,
y: 0,
tileWidth: 32,
tileHeight: 32,
width: 30,
height: 17,
viewportTop: 0, viewportRight: 0, viewportBottom: 0, viewportLeft: 0,
backgroundColor: "rgba(66, 66, 255, 1)",
sprites: [], // Copy for persistence only. Use the direct member sprites which is a collection.
state: "play", // play or pause
time: 0 // Time played in ms
},
shallowAttributes: [
"x", "y", "width", "height", "tileWidth", "tileHeight", "backgroundColor",
"viewportLeft", "viewportRight", "viewportTop", "viewportBottom"
],
viewport: {x: 0, y: 0, width: 0, height: 0},
spriteOptions: {offsetX: 0, offsetY: 0},
initialize: function(attributes, options) {
options || (options = {});
this.input = options.input;
this.camera = options.camera;
this.debugPanel = options.debugPanel;
_.bindAll(this,
"wrapTime",
"save", "getWorldIndex", "getWorldCol", "getWorldRow", "cloneAtPosition",
"spawnSprites", "height", "width", "add", "remove", "setTimeout", "clearTimeout", "getHumanTime"
);
this._times = {};
this.wrapTime("filterAt");
this.wrapTime("findAt");
this.wrapTime("findCollisions");
this.wrapTime("draw");
this.wrapTime("update");
this.wrapTime("drawDynamicSprites");
this.wrapTime("drawStaticSprites");
this.sprites = new Backbone.Collection();
this.quadTree = QuadTree(0, 0, this.width(), this.height());
this.setupSpriteLayers();
this.spawnSprites();
this.on("change:backgroundImage", this.spawnBackgroundImage);
this.spawnBackgroundImage();
this.on("attach", this.onAttach, this);
this.on("detach", this.onDetach, this);
this.on("change:state", this.onStateChange, this);
this.onStateChange();
},
reportTimes: function(fn) {
var total = this.attributes.time;
function reportTime(time, fn) {
console.log(fn + ": " + Math.round(time*10000/total)/100 + "% " + time + "ms");
}
_.each(this._times, reportTime);
reportTime(total, "total");
// Report collision detection as % of update
var time = this._times["findAt"] + this._times["filterAt"] + this._times["findCollisions"];
console.log("collision/update: " + Math.round(time*10000/this._times["update"])/100 + "% " + time + "ms");
},
wrapTime: function(fn) {
var world = this,
origFn = this[fn];
this._times[fn] = 0;
this[fn] = function() {
var now = _.now(),
result = origFn.apply(world, arguments);
if (world.attributes.state == "play") world._times[fn] += _.now() - now;
return result;
}
},
height: function() {
return this.get("height") * this.get("tileHeight");
},
width: function() {
return this.get("width") * this.get("tileWidth");
},
toShallowJSON: function() {
return _.pick(this.attributes, this.shallowAttributes);
},
onAttach: function() {
var engine = this.engine;
this.on("change:viewportLeft change:viewportRight change:viewportTop change:viewportBottom", this.updateViewport);
this.updateViewport();
this.sprites.each(function(sprite) {
sprite.engine = engine;
sprite.trigger("attach", engine);
});
this.listenTo(this.engine, "tap", this.onTap);
this.listenTo(this.engine, "key", this.onKey);
if (this.camera) this.camera.maybePan();
},
onDetach: function() {
this.stopListening(this.engine);
this.sprites.each(function(sprite) {
sprite.engine = undefined;
sprite.trigger("detach");
});
this.off("change:viewportLeft change:viewportRight change:viewportTop change:viewportBottom", this.updateViewport);
},
onStateChange: function() {
var state = this.get("state"),
now = _.now();
if (state == "pause") {
this.accumTime += (now - (this.startTime || now));
this.set("time", this.accumTime);
if (this.attributes.time) this.reportTimes();
} else if (state == "play") {
this.startTime = now;
}
},
getHumanTime: function() {
return _.ms2time(this.attributes.time);
},
updateViewport: function() {
this.viewport.width = this.engine.canvas.width - this.attributes.viewportLeft - this.attributes.viewportRight;
this.viewport.height = this.engine.canvas.height - this.attributes.viewportTop - this.attributes.viewportBottom;
this.spriteOptions.viewportLeft = this.attributes.viewportLeft;
this.spriteOptions.viewportRight = this.attributes.viewportRight;
this.spriteOptions.viewportTop = this.attributes.viewportTop;
this.spriteOptions.viewportBottom = this.attributes.viewportBottom;
this.backgroundCanvas.width = this.engine.canvas.width;
this.backgroundCanvas.height = this.engine.canvas.height;
},
onTap: function(e) {
if (this.attributes.state != "play") return;
e.world = this;
e.worldX = e.canvasX - this.attributes.x;
e.worldY = e.canvasY - this.attributes.y;
this.trigger("tap", e);
},
onKey: function(e) {
if (this.attributes.state != "play") return;
e.world = this;
this.trigger("key", e);
},
// Split static sprites (background tiles) from dynamic ones (animated or moving).
// Draw static on a background and seldomly redraw.
// Dynamic ones are redrawn every animation frame.
// Maintain shadow collections to quickly access the two types.
setupSpriteLayers: function() {
var world = this,
staticSprites = this.staticSprites = new Backbone.Collection(),
dynamicSprites = this.dynamicSprites = new Backbone.Collection();
staticSprites.lookup = {};
staticSprites.maxSpriteWidth = staticSprites.maxSpriteHeight = 0;
dynamicSprites.lookup = {};
dynamicSprites.maxSpriteWidth = dynamicSprites.maxSpriteHeight = 0;
this.quadTree.clear();
function addQto(sprite) {
sprite._qto || (sprite._qto = {});
sprite._qto.id = sprite.id;
sprite._qto.x = sprite.getLeft();
sprite._qto.y = sprite.getTop();
sprite._qto.w = sprite.getRight() - sprite._qto.x;
sprite._qto.h = sprite.getBottom() - sprite._qto.y;
world.quadTree.put(sprite._qto);
}
function removeQto(sprite) {
world.quadTree.remove(sprite._qto, "id");
sprite._qto.id = undefined;
sprite._qto.x = undefined;
sprite._qto.y = undefined;
sprite._qto.w = undefined;
sprite._qto.h = undefined;
}
function add(sprite, collection) {
collection.add(sprite);
index = world.getWorldIndex(sprite);
collection.lookup[index] || (collection.lookup[index] = []);
collection.lookup[index].push(sprite);
sprite.set("lookupIndex", index);
collection.maxSpriteWidth = Math.max(collection.maxSpriteWidth, sprite.attributes.width);
collection.maxSpriteHeight = Math.max(collection.maxSpriteHeight, sprite.attributes.width);
addQto(sprite);
}
function update(sprite, collection) {
var oldIndex = sprite.attributes.lookupIndex,
newIndex = world.getWorldIndex(sprite);
removeQto(sprite);
addQto(sprite);
if (oldIndex == newIndex) return;
if (oldIndex !== undefined && collection.lookup[oldIndex]) {
var pos = _.indexOf(collection.lookup[oldIndex], sprite);
if (pos >= 0) collection.lookup[oldIndex].splice(pos, 1);
}
collection.lookup[newIndex] || (collection.lookup[newIndex] = []);
collection.lookup[newIndex].push(sprite);
sprite.set("lookupIndex", newIndex);
collection.maxSpriteWidth = Math.max(collection.maxSpriteWidth, sprite.attributes.width);
collection.maxSpriteHeight = Math.max(collection.maxSpriteHeight, sprite.attributes.width);
}
function remove(sprite, collection) {
var oldIndex = sprite.attributes.lookupIndex;
if (oldIndex !== undefined && collection.lookup[oldIndex]) {
var pos = _.indexOf(collection.lookup[oldIndex], sprite);
if (pos >= 0) collection.lookup[oldIndex].splice(pos, 1);
}
sprite.unset("lookupIndex");
collection.remove(sprite);
removeQto(sprite);
}
this.listenTo(this.dynamicSprites, "change:x change:y", function(sprite) {
update(sprite, dynamicSprites);
});
this.listenTo(this.sprites, "add", function(sprite) {
if (sprite.get("static"))
add(sprite, staticSprites);
else
add(sprite, dynamicSprites);
world.requestBackgroundRedraw = true;
});
this.listenTo(this.sprites, "remove", function(sprite) {
if (sprite.get("static"))
remove(sprite, staticSprites);
else
remove(sprite, dynamicSprites);
world.requestBackgroundRedraw = true;
});
this.backgroundCanvas = document.createElement("canvas");
this.backgroundCanvas.screencanvas = false;
this.backgroundCanvas.style.display = "none";
document.body.appendChild(this.backgroundCanvas);
this.backgroundContext = this.backgroundCanvas.getContext("2d");
this.previewCanvas = document.createElement("canvas");
this.previewCanvas.screencanvas = false;
this.previewCanvas.style.display = "none";
this.previewCanvas.width = 300;
this.previewCanvas.height = 180;
document.body.appendChild(this.previewCanvas);
this.previewContext = this.previewCanvas.getContext("2d");
drawRect(this.previewContext,
0, 0, this.previewCanvas.width, this.previewCanvas.height,
this.attributes.backgroundColor
);
this.on("change", function() {
this.requestBackgroundRedraw = true;
});
return this;
},
spawnBackgroundImage: function() {
this.backgroundImage = undefined;
var id = this.get("backgroundImage");
if (!id) return;
id = id.replace("#", "");
var img = document.getElementById(id);
if (!img)
throw "Invalid img #" + id + " for world backgroundImage. Cannot find element by id.";
this.backgroundImage = img;
return this;
},
spawnSprites: function() {
var world = this,
w = this.toShallowJSON(),
_sprites = this.get("sprites"),
options = {
input: this.input
};
var sprites = _.clone(this.sprites.models);
_.each(sprites, function(sprite) {
world.remove(sprite);
sprite.input = undefined;
sprite.trigger("detach");
});
_.each(_sprites, function(sprite) {
var s = sprite.attributes ? sprite.attributes : sprite,
cls = _.classify(s.name),
col = world.getWorldCol(s.x),
row = world.getWorldRow(s.y);
var newSprite = new Backbone[cls](_.extend(s,
{
col: col,
row: row
}
), options);
newSprite = world.add(newSprite);
if (world.engine) {
newSprite.engine = world.engine;
newSprite.trigger("attach");
}
if (newSprite.get("hero"))
world.camera.setOptions({world: world, subject: newSprite});
});
this.requestBackgroundRedraw = true;
this.accumTime = this.get("time");
return this;
},
// When saving, persist the sprite collection in the model attribute sprites.
save: function() {
var sprites = this.sprites.reduce(function(sprites, sprite) {
var s = sprite.toSave.apply(sprite);
if (s) sprites.push(s);
return s;
}, []);
// Save a screenshot of 30x15 tiles, skipping the two top first rows
if (this.engine && this.viewport && this.viewport.width) {
var x = this.attributes.viewportLeft + this.attributes.tileWidth*2,
y = this.attributes.viewportTop + this.attributes.tileHeight*2,
width = this.viewport.width - this.attributes.tileWidth*5,
height = this.viewport.height - this.attributes.tileHeight*2;
this.previewContext.drawImage(
this.engine.canvas,
x, y, width, height,
0, 0, this.previewCanvas.width, this.previewCanvas.height
);
}
this.set({sprites: sprites, preview: this.previewCanvas.toDataURL()}, {silent: true});
return Backbone.Model.prototype.save.apply(this, arguments);
},
// Resize the world
resize: function(width, height) {
var deltaY = (height - this.attributes.height) * this.attributes.tileHeight;
if (deltaY == 0) {
this.set({width: width});
return;
}
// When resizing height, add/remove tiles above requiring translation.
var sprites = this.sprites.map(function(sprite) {
var s = sprite.toSave.apply(sprite);
s.y += deltaY;
return s;
});
this.set({
y: this.attributes.y - deltaY,
width: width,
height: height,
sprites: sprites
}, {silent: true});
this.spawnSprites();
return this;
},
timeouts: {},
setTimeout: function(callback, delay) {
var timerId = _.uniqueId();
this.timeouts[timerId] = {
expires: Date.now() + delay,
callback: callback
};
return timerId;
},
clearTimeout: function(timerId) {
if (this.timeouts[timerId] != undefined)
delete this.timeouts[timerId];
},
handleTimeouts: function() {
var now = Date.now(),
timerIds = _.keys(this.timeouts),
timerId, timeout;
for (var i=0; i<timerIds.length; i++) {
timerId = timerIds[i];
timeout = this.timeouts[timerId];
if (now > timeout.expires) {
timeout.callback();
delete this.timeouts[timerId];
}
}
},
update: function(dt) {
if (!this.engine) return false;
var start =_.now(),
sprite, count = 0,
minX = -this.attributes.x + this.attributes.viewportLeft - this.attributes.tileWidth*3,
maxX = -this.attributes.x + this.engine.canvas.width - this.attributes.viewportRight + this.attributes.tileWidth*3,
minY = -this.attributes.y + this.attributes.viewportTop - this.attributes.tileHeight*3,
maxY = -this.attributes.y + this.engine.canvas.height - this.attributes.viewportBottom + this.attributes.tileHeight*3;
// Background
if (this.requestBackgroundRedraw) {
this.requestBackgroundRedraw = false;
this.drawBackground = true;
}
// Foreground
for (var i = 0; i < this.dynamicSprites.models.length; i++) {
var sprite = this.dynamicSprites.models[i];
if (sprite.attributes.x + sprite.attributes.width >= minX && sprite.attributes.x <= maxX &&
sprite.attributes.y + sprite.attributes.height >= minY && sprite.attributes.y <= maxY) {
sprite._draw = sprite.update(dt);
count++;
}
}
if (this.debugPanel)
this.debugPanel.set({
drawBackground: this.drawBackground,
updateCount: count,
updateTime: _.now()-start
});
this.drawBackground = this.drawBackground || this.lastX != this.attributes.x || this.lastY || this.attributes.y;
this.lastX = this.attributes.x; this.lastY = this.attributes.y;
if (this.attributes.state == "play") {
this.handleTimeouts();
this.set({time: this.accumTime + (start - this.startTime)}, {silent: true});
}
return true;
},
draw: function(context) {
if (this._pan && this._pan.startTime)
this._animatePan(context);
if (this.drawBackground) {
this.drawStaticSprites(this.backgroundContext);
this.drawBackground = false;
}
this.drawDynamicSprites(context);
return this;
},
drawStaticSprites: function(context) {
var start =_.now(),
sprite, index, count = 0,
tileX1 = this.getWorldCol(-this.attributes.x + this.attributes.viewportLeft - this.staticSprites.maxSpriteWidth/2),
tileX2 = this.getWorldCol(-this.attributes.x + context.canvas.width - this.attributes.viewportRight + this.staticSprites.maxSpriteWidth/2),
tileY1 = this.getWorldRow(-this.attributes.y + this.attributes.viewportTop - this.staticSprites.maxSpriteHeight/2),
tileY2 = this.getWorldRow(-this.attributes.y + context.canvas.height - this.attributes.viewportBottom + this.staticSprites.maxSpriteHeight/2);
this.spriteOptions.offsetX = this.attributes.x;
this.spriteOptions.offsetY = this.attributes.y;
drawRect(
context,
0, 0, context.canvas.width, context.canvas.height,
this.attributes.backgroundColor
);
if (this.backgroundImage) {
var img = this.backgroundImage,
width = context.canvas.width < img.width ? context.canvas.width : img.width,
height = context.canvas.height < img.height ? context.canvas.height : img.height;
context.drawImage(
img,
0, 0, width, height,
0, 0, width, height
);
}
var secondPass = [];
for (var col = tileX1; col <= tileX2; col++)
for (var row = tileY1; row <= tileY2; row++) {
index = col * this.attributes.height + row;
if (this.staticSprites.lookup[index])
for (var s = 0; s < this.staticSprites.lookup[index].length; s++) {
sprite = this.staticSprites.lookup[index][s];
if (!sprite.attributes.zIndex) {
sprite.draw.call(sprite, context, this.spriteOptions);
count++;
} else {
secondPass.push(sprite);
}
}
}
for (var s = 0; s < secondPass.length; s++) {
sprite = secondPass[s];
sprite.draw.call(sprite, context, this.spriteOptions);
}
if (this.debugPanel) this.debugPanel.set({
staticDrwan: count,
staticDrawTime: _.now()-start
});
return this;
},
drawDynamicSprites: function(context) {
var start =_.now(),
sprite, index, count = 0,
clip = this.attributes.viewportLeft || this.attributes.viewportRight || this.attributes.viewportTop || this.attributes.viewportBottom,
tileX1 = this.getWorldCol(-this.attributes.x + this.attributes.viewportLeft - this.attributes.tileWidth*3),
tileX2 = this.getWorldCol(-this.attributes.x + context.canvas.width - this.attributes.viewportRight + this.attributes.tileWidth*3),
tileY1 = this.getWorldRow(-this.attributes.y + this.attributes.viewportTop - this.attributes.tileHeight*3),
tileY2 = this.getWorldRow(-this.attributes.y + context.canvas.height - this.attributes.viewportBottom + this.attributes.tileHeight*3);
this.spriteOptions.offsetX = this.attributes.x;
this.spriteOptions.offsetY = this.attributes.y;
if (clip) {
context.save();
context.rect(
this.attributes.viewportLeft,
this.attributes.viewportTop,
context.canvas.width - this.attributes.viewportRight,
context.canvas.height - this.attributes.viewportBottom);
context.clip();
}
context.drawImage(this.backgroundCanvas,
this.attributes.viewportLeft, this.attributes.viewportTop, this.viewport.width, this.viewport.height,
this.attributes.viewportLeft, this.attributes.viewportTop, this.viewport.width, this.viewport.height);
var secondPass = [];
for (var col = tileX1; col <= tileX2; col++)
for (var row = tileY1; row <= tileY2; row++) {
index = col * this.attributes.height + row;
if (this.dynamicSprites.lookup[index])
for (var s = 0; s < this.dynamicSprites.lookup[index].length; s++) {
sprite = this.dynamicSprites.lookup[index][s];
if (sprite._draw) {
if (!sprite.attributes.zIndex) {
sprite.draw.call(sprite, context, this.spriteOptions);
count++;
} else {
secondPass.push(sprite);
}
}
}
}
for (var s = 0; s < secondPass.length; s++) {
sprite = secondPass[s];
if (sprite._draw)
sprite.draw.call(sprite, context, this.spriteOptions);
}
if (clip) context.restore();
if (this.debugPanel) this.debugPanel.set({
dynamicDrawn: count,
dynamicDrawTime: _.now()-start
});
return this;
},
// Sprites are ided (and ordered) by columns. This allows for
// fast column drawing without lookup.
getWorldIndex: function(object) {
if (!_.isObject(object)) return null;
var x = object.attributes ? (object.get("x") + object.get("width")/2) : (object.x || 0),
y = object.attributes ? (object.get("y") + object.get("height")/2) : (object.y || 0),
col = Math.floor(x / this.get("tileWidth")),
row = Math.floor(y / this.get("tileHeight"));
return col * this.get("height") + row;
},
isWorldIndexInBoundingBox: function(index, topLeft, bottomRight) {
if (index < topLeft || index > bottomRight) return false;
if (index % this.attributes.height < topLeft % this.attributes.height) return false;
if (index % this.attributes.height > bottomRight % this.attributes.height) return false;
return true;
},
getWorldCol: function(x) {
return Math.floor(x / this.get("tileWidth"));
},
getWorldRow: function(y) {
return Math.floor(y / this.get("tileHeight"));
},
findAt: function(x, y, type, exclude, collision) {
return this._findOrFilter("find", x, y, type, exclude, collision);
},
filterAt: function(x, y, type, exclude, collision) {
return this._findOrFilter("filter", x, y, type, exclude, collision);
},
_findOrFilter: function(fn, x, y, type, exclude, collision) {
var id = exclude && exclude.id ? exclude.id : null,
col = this.getWorldCol(x),
row = this.getWorldRow(y),
index, c, r, s,
result = [];
function test(sprite) {
return (sprite && sprite.id && sprite.id != id) &&
(!type || sprite.get("type") == type) &&
(collision === undefined || sprite.attributes.collision === collision) &&
sprite.overlaps.call(sprite, x, y);
}
if (type == "tile") {
index = this.getWorldIndex({x: x, y: y});
var sprite = index ? this.sprites.get(index) : null;
if (sprite && test(sprite))
return fn == "find" ? sprite : [sprite];
return fn == "find" ? null : result;
}
var matches = this.quadTree.get({x: x, y: y, w: 0, h: 0}),
sprite, i;
for (i = 0; i < matches.length; i++) {
sprite = this.sprites.get(matches[i].id);
if (test(sprite)) result.push(sprite);
}
return fn == "find" ? result[0] || null : result;
},
// Detects collisions on sprites for a set of named coordinates. Works on moving
// and static sprites.
// Map is a map of objects describing the locations to look at, and the result.
// Each map item is an object with:
// - x, y: The lookup coordinate.
// - width, height: Optional. If specified and non-zero, defines an area to lookup (not just a point).
// - dir: The lookout direction; top, right, bottom or left.
// - sprites: array of detected colliding sprites. Reset/initialized to [] every call.
// - sprite: The closest sprite based on the lookout direction.
// Returns the number of found collisions.
findCollisions: function(map, type, exclude, collision) {
if (_.size(map) == 0) return 0;
var id = exclude && exclude.id ? exclude.id : null,
minX, minY, maxX, maxY,
m, c, r, index, s,
count = 0;
for (m in map)
if (map.hasOwnProperty(m)) {
map[m].width || (map[m].width = 0);
map[m].height || (map[m].height = 0);
if (minX == undefined || map[m].x < minX) minX = map[m].x;
if (maxX == undefined || map[m].x + map[m].width > maxX) maxX = map[m].x + map[m].width;
if (minY == undefined || map[m].y < minY) minY = map[m].y;
if (maxY == undefined || map[m].y + map[m].height > maxY) maxY = map[m].y + map[m].height;
map[m].sprites = [];
map[m].sprite = null;
}
function doIt(sprite) {
if (sprite && sprite.id && sprite.id != id &&
(!type || sprite.attributes.type == type) &&
(collision === undefined || sprite.attributes.collision === collision))
for (m in map)
if (map.hasOwnProperty(m) &&
sprite.overlaps.call(sprite, map[m])) {
map[m].sprites.push(sprite);
if (!map[m].sprite) map[m].sprite = sprite;
count++;
}
}
function findClosestSprites() {
for (m in map)
if (map.hasOwnProperty(m) && map[m].sprites.length > 0)
if (map[m].sprites.length == 1)
map[m].sprite = map[m].sprites[0];
else
for (s = 0; s < map[m].sprites.length; s++)
switch (map[m].dir) {
case "left":
c = map[m].sprites[s].getRight(true);
if (c > map[m].x) map[m].sprite = map[m].sprites[s];
break;
case "right":
c = map[m].sprites[s].getLeft(true);
if (c < map[m].x) map[m].sprite = map[m].sprites[s];
break;
case "top":
c = map[m].sprites[s].getBottom(true);
if (c > map[m].y) map[m].sprite = map[m].sprites[s];
break;
case "bottom":
c = map[m].sprites[s].getTop(true);
if (c < map[m].y) map[m].sprite = map[m].sprites[s];
break;
}
}
var matches = this.quadTree.get({x: minX, y: minY, w: maxX-minX, h: maxY-minY}),
sprite, i;
for (i = 0; i < matches.length; i++)
doIt(this.sprites.get(matches[i].id));
findClosestSprites();
return count;
},
// Static tiles lookup.
// Note: Will not detect moving sprites!
// DEPRECATED: Use findAt instead
findCollidingAt:function(x, y) {
return this.findAt(x, y, "tile", null, true);
},
add: function(models, options) {
if (_.isArray(models))
for (var i = 0; i < models.length; i++) {
if (models[i].attributes)
models[i].set("id", this.buildId(models[i]));
else
models[i].id = this.buildId(models[i]);
}
else {
if (models.attributes)
models.set("id", this.buildId(models));
else
models.id = this.buildId(models);
}
models = this.sprites.add.call(this.sprites, models, options);
if (_.isArray(models)) {
for (var i = 0; i < models.length; i++) {
models[i].world = this;
if (!options || !options.silent)
models[i].trigger("addWorld", models[i], this, options);
}
} else {
models.world = this;
if (!options || !options.silent)
models.trigger("addWorld", models, this, options);
}
return models;
},
remove: function(models, options) {
models = this.sprites.remove.apply(this.sprites, arguments);
if (_.isArray(models)) {
for (var i = 0; i < models.length; i++)
if (models[i].world === this) {
if (!options || !options.silent)
models[i].trigger("removeWorld", models[i], this, options);
models[i].world = undefined;
}
} else if (models && models.world === this) {
if (!options || !options.silent)
models.trigger("removeWorld", models, this, options);
models.world = undefined;
}
return models;
},
cloneAtPosition: function(sprite, x, y, options) {
options || (options = {});
options.world = this;
options.input = this.input;
var w = this.toShallowJSON(),
existing = this.findAt(x, y),
existingName = existing ? existing.get("name") : null,
spriteName = sprite ? sprite.get("name") : "";
if (!sprite && !existing) return null;
if (!sprite && existing) {
this.sprites.remove(existing);
return null;
}
if (existing) {
if (spriteName == existingName) {
if (!existing.getStateInfo) {
this.sprites.remove(existing);
return null;
}
// Toggle if same sprite - either turn around or remove
var cur = existing.getStateInfo(),
removeOnDir = sprite.get("hero") ? "left" : "right";
if (!cur.dir || cur.dir == removeOnDir) {
this.remove(existing);
return null;
}
if (cur.opo && _.isFunction(existing.toggleDirection))
existing.toggleDirection(cur.opo);
return existing;
} else {
// Replace existing
this.remove(existing);
}
}
var col = this.getWorldCol(x),
row = this.getWorldRow(y - sprite.get("height") + w.tileHeight),
cls = _.classify(spriteName);
var newSprite = new Backbone[cls](_.extend({}, sprite.toJSON(), {
x: col * w.tileWidth,
y: row * w.tileHeight
}), options);
// A hero is a singleton
var isHero = newSprite.get("hero");
if (isHero) {
var oldHeros = this.sprites.where({hero: true});
if (oldHeros.length) this.sprites.remove(oldHeros);
}
this.add(newSprite, options);
if (isHero && this.camera)
this.camera.setOptions({world: this, subject: newSprite});
newSprite.engine = this.engine;
newSprite.trigger("attach", this.engine);
return newSprite;
},
buildIdFromName: function(name) {
var re = new RegExp("^" + name + "\\." + "\\d+$"),
numbers = this.dynamicSprites.reduce(function(numbers, sprite) {
if (sprite.id && sprite.id.length && sprite.id.match(re))
numbers.push(parseInt(sprite.id.replace(name + ".", "")));
return numbers;
}, [0]);
return name + "." + (_.max(numbers) + 1);
},
buildId: function(sprite) {
var attributes = sprite.attributes || sprite;
if (attributes.type != "tile")
return this.buildIdFromName(attributes.name);
return this.getWorldCol(attributes.x) * this.attributes.height +
this.getWorldRow(attributes.y);
},
clearBeyondWorldBoundaries: function() {
var minX = 0,
minY = 0,
maxX = this.attributes.width * this.attributes.tileWidth,
maxY = this.attributes.height * this.attributes.tileHeight,
toRemove = this.sprites.reduce(function(result, sprite) {
if (sprite.attributes.x + sprite.attributes.width < minX ||
sprite.attributes.y + sprite.attributes.height < minY ||
sprite.attributes.x > maxX ||
sprite.attributes.y > maxY)
result.push(sprite);
return result;
});
this.sprites.remove(toRemove);
return this;
},
pan: function(targetX, targetY, callback, easing, easingTime) {
if (this.get("state") == "play") throw "Cannot pan world in play.";
this._pan || (this._pan = {});
this._pan.startTime = _.now();
this._pan.startX = this.get("x");
this._pan.startY = this.get("y");
this._pan.targetX = targetX;
this._pan.targetY = targetY;
this._pan.easing = easing || "easeOutQuint";
this._pan.easingTime = easingTime || 1000;
this._pan.callback = callback;
return this;
},
_animatePan: function() {
var now = _.now();
if (now < this._pan.startTime + this._pan.easingTime) {
var factor = Backbone.EasingFunctions[this._pan.easing]((now - this._pan.startTime) / this._pan.easingTime);
this.set({
x: this._pan.startX + factor * (this._pan.targetX - this._pan.startX),
y: this._pan.startY + factor * (this._pan.targetY - this._pan.startY)
});
} else {
this._endPan(true);
}
},
_endPan: function(trigger) {
if (trigger && typeof this._pan.callback == "function")
_.defer(this._pan.callback.bind(this));
if (this._pan.targetX || this._pan.targetY)
this.set({x: this._pan.targetX, y: this._pan.targetY});
this._pan.startTime = undefined;
this._pan.startX = undefined;
this._pan.startY = undefined;
this._pan.targetX = undefined;
this._pan.targetY = undefined;
this._pan.callback = undefined;
return this;
}
});
}).call(this);

View File

@@ -0,0 +1,103 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
Backbone.Pennie = Backbone.AnimatedTile.extend({
defaults: {
name: "pennie",
type: "artifact",
width: 32,
height: 32,
spriteSheet: "tiles",
state: "idle",
collision: true
},
animations: {
idle: {
sequences: [52, 52, 53, 54, 53, 52],
delay: 50
}
},
initialize: function(attributes, options) {
Backbone.AnimatedTile.prototype.initialize.apply(this, arguments);
this.on("hit", this.hit, this);
this.on("squish", this.hit, this);
},
hit: function(sprite, dir, dir2) {
if (sprite.get("hero")) {
sprite.trigger("hit", this);
_.defer(this.world.remove, this);
}
}
});
Backbone.PennieUg = Backbone.Pennie.extend({
defaults: _.extend(_.deepClone(Backbone.Pennie.prototype.defaults), {
name: "pennie-ug"
}),
animations: _.deepClone(Backbone.Pennie.prototype.animations)
});
Backbone.PennieUg.prototype.animations.idle.sequences = [168, 168, 169, 170, 169, 168];
Backbone.Lever = Backbone.Pennie.extend({
defaults: _.extend(_.deepClone(Backbone.Pennie.prototype.defaults), {
name: "lever"
}),
animations: _.deepClone(Backbone.Pennie.prototype.animations),
hit: function(sprite, dir, dir2) {}
});
Backbone.Lever.prototype.animations.idle.sequences = [55, 55, 56, 57, 56, 55];
Backbone.FlyingPennie = Backbone.Sprite.extend({
defaults: {
name: "flying-pennie",
type: "decoration",
width: 32,
height: 32,
spriteSheet: "tiles",
state: "anim",
collision: false
},
animations: {
anim: {
sequences: [
{frame: 52, x: 0, y: -32, scaleX: 1.00, scaleY: 1},
{frame: 52, x: 0, y: -64, scaleX: 0.50, scaleY: 1},
{frame: 53, x: 0, y: -90, scaleX: 0.50, scaleY: 1},
{frame: 53, x: 0, y: -128, scaleX: 1.00, scaleY: 1},
{frame: 53, x: 0, y: -128, scaleX: 0.50, scaleY: 1},
{frame: 52, x: 0, y: -112, scaleX: 0.50, scaleY: 1},
{frame: 52, x: 0, y: -90, scaleX: 1.00, scaleY: 1},
{frame: 52, x: 0, y: -80, scaleX: 0.50, scaleY: 1},
{frame: 53, x: 0, y: -80, scaleX: 0.50, scaleY: 1}
],
delay: 50
}
},
initialize: function(attributes, options) {
options || (options = {});
this.world = options.world;
this.lastSequenceChangeTime = 0;
},
update: function(dt) {
Backbone.Sprite.prototype.update.call(this, arguments);
var animation = this.getAnimation(),
sequenceIndex = this.get("sequenceIndex");
if (sequenceIndex >= animation.sequences.length-1) {
_.defer(this.world.remove, this);
return false;
}
return true;
}
});
}).call(this);

View File

@@ -0,0 +1,88 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
Backbone.Display = Backbone.Model.extend({
defaults: {
pennies: 0
},
initialize: function(attributes, options) {
options || (options = {});
this.world = options.world;
this.pennieSprite = new Backbone.AnimatedTile({
name: "pennie",
type: "character",
x: 150,
y: 4,
width: 32,
height: 32,
spriteSheet: "tiles",
state: "idle"
});
this.pennieSprite.animations = {
idle: {
sequences: [52, 52, 53, 54, 53, 52],
delay: 50,
scaleX: 0.75,
scaleY: 0.75
}
};
this.on("attach", this.onAttach, this);
this.on("detach", this.onDetach, this);
},
onAttach: function() {
this.pennieSprite.engine = this.engine;
this.pennieSprite.trigger("attach");
this.listenTo(this.world.dynamicSprites, "remove", this.onPennieRemoved);
this.pennieSprite.set({x: this.engine.canvas.width/2 - 30});
},
onDetach: function() {
this.pennieSprite.trigger("detach");
this.pennieSprite.engine = undefined;
this.stopListening();
},
update: function(dt) {
return true;
},
draw: function(context) {
var text = "×" + (this.attributes.pennies < 10 ? "0" : "") + this.attributes.pennies;
context.fillStyle = "#fff";
context.font = "20px arcade, Verdana, Arial, Sans-Serif";
context.textBaseline = "top";
context.fontWeight = "normal";
context.textAlign = "left";
context.fillText(text, context.canvas.width/2 - 100, 12);
this.pennieSprite.draw.call(this.pennieSprite, context);
context.textAlign = "right";
context.fillText(this.world.attributes.name.replace(/_/g, " "), context.canvas.width - 100, 12);
return this;
},
onPennieRemoved: function(sprite) {
if (this.world.get("state") != "play") return;
var name = sprite.get("name"),
pennies = this.get("pennies");
if (name.indexOf("pennie") != -1) {
pennies += 1;
if (pennies > 99) pennies = 0;
this.set("pennies", pennies);
}
}
});
}).call(this);

View File

@@ -0,0 +1,313 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var sequenceDelay = 300,
animations;
// Mushroom is the base enemie class.
Backbone.Mushroom = Backbone.Character.extend({
defaults: _.extend(_.deepClone(Backbone.Character.prototype.defaults), {
name: "mushroom",
type: "character",
width: 32,
height: 64,
paddingTop: 32,
spriteSheet: "enemies",
state: "idle-left",
velocity: 0,
yVelocity: 0,
collision: true,
aiDelay: 0
}),
animations: _.extend(_.deepClone(Backbone.Character.prototype.animations), {
"squished-left": {
sequences: [2],
velocity: 0,
scaleX: 1,
scaleY: 1
},
"squished-right": {
sequences: [2],
velocity: 0,
scaleX: -1,
scaleY: 1
}
}),
ai: function(dt) {
var cur = this.getStateInfo();
if (cur.mov == "squished" && !this.get("collision")) this.cancelUpdate = true;
return this;
},
isAttacking: function(sprite, dir, dir2) {
if (this.cancelUpdate) return false;
var cur = this.getStateInfo();
return (cur.mov == "walk" || cur.mov == "idle");
},
squish: function(sprite) {
var self = this,
cur = this.getStateInfo();
this.set({
state: this.buildState("squished", cur.dir),
collision: false
});
this.world.setTimeout(function() {
if (self && self.world) self.world.remove(self);
}, 2000);
this.cancelUpdate = true;
return this;
},
hit: function(sprite, dir, dir2) {
if (this._handlingSpriteHit) return this;
this._handlingSpriteHit = sprite;
var cur = this.getStateInfo(),
opo = dir == "left" ? "right" : (dir == "right" ? "left" : (dir == "top" ? "bottom" : "top"));
if (sprite.get("hero")) {
if (dir == "top")
this.squish.apply(this, arguments);
} else if (sprite.get("state").indexOf("slide") != -1 ||
sprite.get("type") == "tile" && dir == "bottom" && sprite.get("state") == "bounce") {
this.knockout.apply(this, arguments);
}
sprite.trigger("hit", this, opo);
this._handlingSpriteHit = undefined;
return this;
}
});
Backbone.Turtle = Backbone.Mushroom.extend({
defaults: _.extend(_.deepClone(Backbone.Mushroom.prototype.defaults), {
name: "turtle"
}),
animations: _.deepClone(Backbone.Mushroom.prototype.animations),
isAttacking: function() {
var cur = this.getStateInfo();
return (cur.mov == "walk" || cur.mov == "idle");
},
slide: function(sprite, dir, dir2) {
if (this.wakeTimerId) {
this.world.clearTimeout(this.wakeTimerId);
this.wakeTimerId = null;
}
var dir = sprite.getCenterX(true) > this.getCenterX(true) ? "left" : "right";
this.set("state", this.buildState("walk", "slide", dir));
this.cancelUpdate = true;
return this;
},
squish: function(sprite, dir, dir2) {
var cur = this.getStateInfo();
if (cur.mov == "squished" || cur.mov == "wake")
return this.slide.apply(this, arguments);
if (this.wakeTimerId) {
this.world.clearTimeout(this.wakeTimerId);
this.wakeTimerId = null;
}
this.set("state", this.buildState("squished", cur.dir));
this.wakeTimerId = this.world.setTimeout(this.wake.bind(this), 5000);
this.cancelUpdate = true;
return this;
},
hit: function(sprite, dir, dir2) {
if (this._handlingSpriteHit) return this;
this._handlingSpriteHit = sprite;
var cur = this.getStateInfo(),
opo = dir == "left" ? "right" : (dir == "right" ? "left" : (dir == "top" ? "bottom" : "top"));
if (cur.mov2 == "slide") this.cancelUpdate = true;
if (dir == "top") {
this.squish.apply(this, arguments);
} else if (sprite.get("hero") && (cur.mov == "squished" || cur.mov == "wake")) {
this.slide.apply(this, arguments);
opo = "bottom";
} else if (sprite.get("state").indexOf("slide") != -1 ||
sprite.get("type") == "tile" && dir == "bottom" && sprite.get("state") == "bounce") {
this.knockout.apply(this, arguments);
}
sprite.trigger("hit", this, opo);
this._handlingSpriteHit = undefined;
return this;
},
wake: function() {
var cur = this.getStateInfo();
if (this.wakeTimerId) {
this.world.clearTimeout(this.wakeTimerId);
this.wakeTimerId = null;
}
if (cur.mov == "squished") {
this.set("state", this.buildState("wake", cur.dir));
this.wakeTimerId = this.world.setTimeout(this.wake.bind(this), 5000);
} else if (cur.mov == "wake") {
this.set("state", this.buildState("walk", cur.dir));
}
return this;
}
});
animations = Backbone.Turtle.prototype.animations;
animations["idle-left"].sequences = animations["idle-right"].sequences =
animations["fall-left"].sequences = animations["fall-right"].sequences =
animations["ko-left"].sequences = animations["ko-right"].sequences = [6];
animations["walk-left"].sequences = animations["walk-right"].sequences = [6, 7];
animations["squished-left"].sequences = animations["squished-right"].sequences = [10];
_.extend(animations, {
"wake-left": {
sequences: [10, 11],
velocity: 0,
scaleX: 1,
scaleY: 1,
delay: sequenceDelay
},
"wake-right": {
sequences: [10, 11],
velocity: 0,
scaleX: -1,
scaleY: 1,
delay: sequenceDelay
},
"walk-slide-left": {
sequences: [10],
velocity: -300,
scaleX: 1,
scaleY: 1
},
"walk-slide-right": {
sequences: [10],
velocity: 300,
scaleX: -1,
scaleY: 1
},
"fall-slide-left": {
sequences: [10],
velocity: -300,
yVelocity: animations["fall-left"].yVelocity,
yAcceleration: animations["fall-left"].yAcceleration,
scaleX: 1,
scaleY: 1
},
"fall-slide-right": {
sequences: [10],
velocity: 300,
yVelocity: animations["fall-right"].yVelocity,
yAcceleration: animations["fall-right"].yAcceleration,
scaleX: -1,
scaleY: 1
}
});
Backbone.FlyingTurtle = Backbone.Turtle.extend({
defaults: _.extend(_.deepClone(Backbone.Turtle.prototype.defaults), {
name: "flying-turtle"
}),
animations: _.deepClone(Backbone.Turtle.prototype.animations),
fallbackSprite: Backbone.Turtle,
onUpdate: function(dt) {
var cur = this.getStateInfo(),
animation = this.getAnimation(),
attrs = {};
if (cur.mov2 == null && cur.mov == "walk" && this.world.get("state") == "play") {
attrs.state = this.buildState("fall", cur.dir);
attrs.yVelocity = -this.animations["fall-right"].yVelocity;
}
if (!_.isEmpty(attrs)) this.set(attrs);
return true;
},
squish: function(sprite) {
var cur = this.getStateInfo();
var newSprite = new this.fallbackSprite({
x: this.get("x"),
y: this.get("y"),
state: "walk-" + cur.dir
});
newSprite.set("id", this.world.buildIdFromName(newSprite.get("name")));
this.world.add(newSprite);
this.world.remove(this);
this.cancelUpdate = true;
}
});
animations = Backbone.FlyingTurtle.prototype.animations;
animations["idle-left"].sequences = animations["idle-right"].sequences =
animations["fall-left"].sequences = animations["fall-right"].sequences =
animations["ko-left"].sequences = animations["ko-right"].sequences = [8];
animations["walk-left"].sequences = animations["walk-right"].sequences = [8, 9];
Backbone.RedTurtle = Backbone.Turtle.extend({
defaults: _.extend(_.deepClone(Backbone.Turtle.prototype.defaults), {
name: "red-turtle"
}),
animations: _.deepClone(Backbone.Turtle.prototype.animations)
});
animations = Backbone.RedTurtle.prototype.animations;
animations["idle-left"].sequences = animations["idle-right"].sequences =
animations["fall-left"].sequences = animations["fall-right"].sequences =
animations["ko-left"].sequences = animations["ko-right"].sequences = [108];
animations["walk-left"].sequences = animations["walk-right"].sequences = [108, 109];
animations["squished-left"].sequences = animations["squished-right"].sequences =
animations["walk-slide-left"].sequences = animations["walk-slide-right"].sequences =
animations["fall-slide-left"].sequences = animations["fall-slide-right"].sequences = [112];
animations["wake-left"].sequences = animations["wake-right"].sequences = [112, 113];
Backbone.RedFlyingTurtle = Backbone.FlyingTurtle.extend({
defaults: _.extend(_.deepClone(Backbone.FlyingTurtle.prototype.defaults), {
name: "red-flying-turtle"
}),
animations: _.deepClone(Backbone.FlyingTurtle.prototype.animations),
fallbackSprite: Backbone.RedTurtle
});
animations = Backbone.RedFlyingTurtle.prototype.animations;
animations["idle-left"].sequences = animations["idle-right"].sequences =
animations["fall-left"].sequences = animations["fall-right"].sequences =
animations["ko-left"].sequences = animations["ko-right"].sequences = [110];
animations["walk-left"].sequences = animations["walk-right"].sequences = [110, 111];
animations["squished-left"].sequences = animations["squished-right"].sequences =
animations["walk-slide-left"].sequences = animations["walk-slide-right"].sequences =
animations["fall-slide-left"].sequences = animations["fall-slide-right"].sequences = [112];
animations["wake-left"].sequences = animations["wake-right"].sequences = [112, 113];
Backbone.Beetle = Backbone.Turtle.extend({
defaults: _.extend(_.deepClone(Backbone.Turtle.prototype.defaults), {
name: "beetle"
}),
animations: _.deepClone(Backbone.Turtle.prototype.animations)
});
animations = Backbone.Beetle.prototype.animations;
animations["idle-left"].sequences = animations["idle-right"].sequences =
animations["fall-left"].sequences = animations["fall-right"].sequences =
animations["ko-left"].sequences = animations["ko-right"].sequences = [33];
animations["walk-left"].sequences = animations["walk-right"].sequences = [33, 32];
animations["squished-left"].sequences = animations["squished-right"].sequences =
animations["walk-slide-left"].sequences = animations["walk-slide-right"].sequences =
animations["fall-slide-left"].sequences = animations["fall-slide-right"].sequences =
animations["wake-left"].sequences = animations["wake-right"].sequences = [34];
Backbone.Spike = Backbone.Mushroom.extend({
defaults: _.extend(_.deepClone(Backbone.Mushroom.prototype.defaults), {
name: "spike"
}),
animations: _.deepClone(Backbone.Mushroom.prototype.animations),
squish: function() {}
});
animations = Backbone.Spike.prototype.animations;
animations["idle-left"].sequences = animations["idle-right"].sequences =
animations["fall-left"].sequences = animations["fall-right"].sequences =
animations["ko-left"].sequences = animations["ko-right"].sequences = [133];
animations["walk-left"].sequences = animations["walk-right"].sequences = [133, 132];
}).call(this);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,113 @@
<!doctype html>
<html style="touch-action: none;" manifest="offline.appcache">
<head>
<title>Super Mario Bros Level 1-1 - Backbone Game Engine</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="../favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="../apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=960, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"/>
<meta name="mobileoptimized" content="0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<script src="../3rd/qtree.js" type="text/javascript"></script>
<script src="../3rd/underscore.js" type="text/javascript"></script>
<script src="../3rd/backbone.native.js" type="text/javascript"></script>
<script src="../3rd/backbone.js" type="text/javascript"></script>
<script src="../src/adjust-viewport.js" type="text/javascript"></script>
<script src="../src/shapes.js" type="text/javascript"></script>
<script src="../src/core.js" type="text/javascript"></script>
<script src="../src/character.js" type="text/javascript"></script>
<script src="../src/input.js" type="text/javascript"></script>
<script src="../src/hero.js" type="text/javascript"></script>
<script src="../src/world.js" type="text/javascript"></script>
<script src="../src/local-storage.js" type="text/javascript"></script>
<script src="../src/camera.js" type="text/javascript"></script>
<script src="../src/editor.js" type="text/javascript"></script>
<script src="mario.js" type="text/javascript"></script>
<script src="tiles.js" type="text/javascript"></script>
<script src="artifacts.js" type="text/javascript"></script>
<script src="enemies.js" type="text/javascript"></script>
<script src="display.js" type="text/javascript"></script>
<script src="level_1-1.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background-color: #000;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
/* canvas 自适应容器 */
canvas {
width: 100%;
height: auto;
}
</style>
<script>
// 强制启用触摸板
window.forceTouchpad = true;
</script>
</head>
<body>
<img id="mario" src="super-mario-2x.png" style="display:none;" />
<img id="tiles" src="super-mario-tiles-2x.png" style="display:none;" />
<img id="enemies" src="super-mario-enemies-2x.png" style="display:none;" />
<img id="icons" src="icons.png" style="display:none;" />
<canvas id="foreground" width="960" height="700">
Your browser does not support canvas element.
</canvas>
<script>
(function () {
const canvas = document.getElementById("foreground");
if (!canvas) return;
function fitCanvas() {
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
if (!viewportWidth || !viewportHeight) return;
const ratio = canvas.width / canvas.height;
let w = viewportWidth;
let h = viewportWidth / ratio;
if (h > viewportHeight) {
h = viewportHeight;
w = viewportHeight * ratio;
}
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.style.left = (viewportWidth - w) / 2 + "px";
canvas.style.top = (viewportHeight - h) / 2 + "px";
}
window.addEventListener("resize", fitCanvas, { passive: true });
window.addEventListener("orientationchange", fitCanvas, { passive: true });
fitCanvas();
setTimeout(fitCanvas, 50);
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
$(window).on("load", function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
var canvas = document.getElementById("foreground"),
context = canvas.getContext("2d");
adjustViewport(canvas);
var spriteNames = [
"ground", "brick-top", "brick", "ground2", "block", "block2", "question-block", "pennie",
"tube1", "tube2", "tube3-mirror", "tube4-mirror", "bush1", "bush2", "bush3", "cloud1", "cloud2", "cloud3",
"cloud-happy1", "cloud-happy2", "cloud-happy3", "flag-pole1", "water-bridge",
"cloud-small", "ground-ug", "brick-top-ug", "brick-ug", "ground2-ug", "block-ug", "block2-ug",
"question-block-ug", "pennie-ug", "tube3", "tube4", "tube1-mirror", "tube2-mirror", "bush4", "bush5", "bush6",
"cloud4", "cloud5", "cloud6", "cloud-happy4", "cloud-happy5", "cloud-happy6", "flag-pole2", "railing",
"tube1-out", "tube2-out", "tube3-out", "tube1-out-mirror", "tube2-out-mirror", "tube3-out-mirror", "water1", "lava1",
"platform1", "platform2", "platform3", "brick7-castle", "brick-castle", "brick2-castle", "brick3-castle",
"mario", "luigi", "mushroom", "turtle", "flying-turtle", "red-turtle", "red-flying-turtle", "beetle", "spike",
"tube4-out", "tube5-out", "tube6-out", "tube4-out-mirror", "tube5-out-mirror", "tube6-out-mirror", "water2", "lava2",
"bush7", "bush8", "bush9", "platform-pole", "brick4-castle", "brick5-castle", "brick6-castle"
];
Backbone.Controller = Backbone.Model.extend({
initialize: function(attributes, options) {
options || (options = {});
var controller = this;
_.bindAll(this, "onChangeState", "toggleState", "saveWorld", "loadWorld");
// Create our sprite sheets and attach them to existing sprite classes
this.spriteSheets = new Backbone.SpriteSheetCollection([{
id: "mario",
img: "#mario",
tileWidth: 32,
tileHeight: 64,
tileColumns: 21,
tileRows: 6
}, {
id: "tiles",
img: "#tiles",
tileWidth: 32,
tileHeight: 32,
tileColumns: 29,
tileRows: 28
}, {
id: "enemies",
img: "#enemies",
tileWidth: 32,
tileHeight: 64,
tileColumns: 51,
tileRows: 3
}]).attachToSpriteClasses();
// Create the debug panel
this.debugPanel = new Backbone.DebugPanel();
// User input (turn off touchpad to start)
this.input = new Backbone.Input({
drawTouchpad: true
});
// Camera
this.camera = new Backbone.Camera();
// Our world
// Reserve bottom of canvas for input and editor
this.world = new Backbone.World(
_.extend({viewportBottom: 156}, window._world), {
input: this.input,
camera: this.camera
});
this.display = new Backbone.Display({}, {
world: this.world
});
// Message
this.message = new Backbone.Message({
x: 480, y: 20
});
// Buttons
this.toggleButton = new Backbone.Button({
x: 4, y: 4, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 0, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.toggleButton.on("tap", this.toggleState, this);
this.saveButton = new Backbone.Button({
x: 4, y: 548, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 96, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.saveButton.on("tap", this.saveWorld, this);
this.restartButton = new Backbone.Button({
x: 4, y: 608, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 128, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.restartButton.on("tap", this.restartWorld, this);
this.downloadButton = new Backbone.Button({
x: 888, y: 10, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 64, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
this.downloadButton.on("tap", this.downloadNewVersion, this);
// The game engine
this.engine = new Backbone.Engine({}, {
canvas: canvas,
debugPanel: this.debugPanel
});
this.engine.add(_.compact([
this.world,
this.display,
this.camera,
this.toggleButton,
this.message,
this.debugPanel
]));
// The sprite picker and editor
this.editor = new Backbone.WorldEditor({
spriteNames: spriteNames
}, {
world: this.world
});
// Controls
$(document).on("keypress.Controller", function(e) {
if (e.keyCode == 66 || e.keyCode == 98)
controller.engine.toggle(); // b to break the animation
else if (e.keyCode == 80 || e.keyCode == 112)
controller.toggleState(); // p to pause and pause
});
this.listenTo(this.world, "change:state", this.onChangeState);
this.onChangeState();
// If we have Application Cache active, load the latest world
this.loadWorld();
},
toggleState: function(e) {
var state = this.world.get("state");
this.world.set("state", state == "pause" ? "play" : "pause");
if (!this.engine.isRunning()) this.engine.start();
},
onChangeState: function() {
var state = this.world.get("state");
if (state == "pause") {
// Edit
context.clearRect(0, 0, canvas.width, canvas.height);
this.engine.remove(this.input);
this.engine.add(this.editor);
this.engine.add([this.saveButton, this.restartButton]);
this.toggleButton.set({imgX: 32});
} else {
// Play
context.clearRect(0, 0, canvas.width, canvas.height);
this.engine.remove(this.editor);
this.engine.remove([this.saveButton, this.restartButton]);
this.engine.add(this.input);
this.toggleButton.set({imgX: 0});
}
},
// Save our world to the server when changed. Debounce by 5 seconds
// and push back saving upon activity
saveWorld: function() {
var controller = this,
world = this.world
message = this.message;
message.show("Saving...");
world.save(null, {
success: function() {
setTimeout(function() {
message.hide();
}, 1000);
},
error: function(xhr, textStatus, errorThrown ) {
message.show("Error: " + errorThrown);
}
});
return this;
},
// Load our world from the server.
loadWorld: function() {
var controller = this,
world = this.world,
message = this.message;
message.show("Loading...");
world.fetch({
success: function() {
world.spawnSprites();
message.hide();
},
error: function(xhr, textStatus, errorThrown ) {
message.show('Error: ' + errorThrown);
setTimeout(function() {
message.hide();
}, 2000);
}
});
return this;
},
restartWorld: function() {
var controller = this,
world = this.world,
message = this.message;
message.show("Restarting...");
localStorage.removeItem(world.id);
world.set(window._world).spawnSprites();
setTimeout(function() {
message.hide();
}, 2000);
return this;
},
downloadNewVersion: function() {
window.applicationCache.swapCache();
this.message.show("Please wait...");
window.location.reload();
}
});
var controller = new Backbone.Controller();
// When a newer version is available, load it and inform
// the user it can be downloaded.
if (window.applicationCache !== undefined)
window.applicationCache.addEventListener('updateready', function() {
controller.engine.add(controller.downloadButton);
});
// Expose things as globals - easier to debug
_.extend(window, {
canvas: canvas,
context: context,
controller: controller,
});
});

View File

@@ -0,0 +1,68 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
Backbone.Mario = Backbone.Hero.extend({
defaults: _.extend({}, Backbone.Hero.prototype.defaults, {
name: "mario",
spriteSheet: "mario"
}),
bounce: function(sprite, dir, dir2) {
var cur = this.getStateInfo(),
state = this.buildState("jump", cur.dir);
this.set({
state: state,
yVelocity: this.animations[state].yStartVelocity*0.5,
nextState: this.buildState("idle", cur.dir)
});
this.cancelUpdate = true;
return this;
},
hit: function(sprite, dir, dir2) {
if (this._handlingSpriteHit) return this;
this._handlingSpriteHit = sprite;
if (sprite.get("type") == "artifact") {
this.cancelUpdate = true;
} else if (sprite.get("type") == "character") {
var name = sprite.get("name"),
cur = this.getStateInfo(),
opo = dir == "left" ? "right" : (dir == "right" ? "left" : (dir == "top" ? "bottom" : "top"));
if (dir == "bottom" && name != "spike") {
this.bounce.apply(this, arguments);
} else if (sprite.isAttacking()) {
this.knockout(sprite, "left");
}
sprite.trigger("hit", this, opo);
}
this._handlingSpriteHit = undefined;
return this;
}
});
Backbone.Luigi = Backbone.Mario.extend({
defaults: _.extend({}, Backbone.Hero.prototype.defaults, {
name: "luigi",
spriteSheet: "mario"
}),
animations: _.reduce(Backbone.Hero.prototype.animations, function(animations, anim, name) {
var clone = _.clone(anim);
clone.sequences = _.map(anim.sequences, function(index) {
return index + 42;
});
animations[name] = clone;
return animations;
}, {})
});
}).call(this);

View File

@@ -0,0 +1,31 @@
CACHE MANIFEST
# Version 0.40 (c) 2014 Martin Drapeau
../3rd/qtree.js
../3rd/underscore.js
../3rd/backbone.native.js
../3rd/backbone.js
../src/adjust-viewport.js
../src/input.js
../src/shapes.js
../src/core.js
../src/character.js
../src/hero.js
../src/world.js
../src/local-storage.js
../src/camera.js
../src/editor.js
mario.js
tiles.js
artifacts.js
enemies.js
display.js
level_1-1.js
main.js
super-mario-2x.png
super-mario-tiles-2x.png
super-mario-enemies-2x.png
icons.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,364 @@
(function() {
/**
*
* Backbone Game Engine - An elementary HTML5 canvas game engine using Backbone.
*
* Copyright (c) 2014 Martin Drapeau
* https://github.com/martindrapeau/backbone-game-engine
*
*/
Backbone.Tile = Backbone.Sprite.extend({
defaults: {
type: "tile",
width: 32,
height: 32,
spriteSheet: "tiles",
state: "idle",
static: true,
persist: true
},
initialize: function(attributes, options) {
options || (options = {});
this.world = options.world;
this.lastSequenceChangeTime = 0;
}
});
function extendSprite(cls, name, attributes, animations) {
var newCls = _.classify(name);
Backbone[newCls] = Backbone[cls].extend({
defaults: _.extend(
_.deepClone(Backbone[cls].prototype.defaults),
{name: name},
attributes || {}
),
animations: _.extend(
_.deepClone(Backbone[cls].prototype.animations),
animations || {}
)
});
return Backbone[newCls];
}
extendSprite("Tile", "ground", {collision: true}, {idle: {sequences: [0]}});
extendSprite("Tile", "ground2", {collision: true}, {idle: {sequences: [31]}});
extendSprite("Tile", "block", {collision: true}, {idle: {sequences: [3]}});
extendSprite("Tile", "block2", {collision: true}, {idle: {sequences: [29]}});
extendSprite("Tile", "tube1", {collision: true}, {idle: {sequences: [290]}});
extendSprite("Tile", "tube2", {collision: true}, {idle: {sequences: [291]}});
extendSprite("Tile", "tube1-mirror", {collision: true}, {idle: {sequences: [261]}});
extendSprite("Tile", "tube2-mirror", {collision: true}, {idle: {sequences: [262]}});
extendSprite("Tile", "tube1-ug", {collision: true}, {idle: {sequences: [406]}});
extendSprite("Tile", "tube2-ug", {collision: true}, {idle: {sequences: [407]}});
extendSprite("Tile", "bush1", {collision: false}, {idle: {sequences: [240]}});
extendSprite("Tile", "bush2", {collision: false}, {idle: {sequences: [241]}});
extendSprite("Tile", "bush3", {collision: false}, {idle: {sequences: [242]}});
extendSprite("Tile", "cloud1", {collision: false}, {idle: {sequences: [580]}});
extendSprite("Tile", "cloud2", {collision: false}, {idle: {sequences: [581]}});
extendSprite("Tile", "cloud3", {collision: false}, {idle: {sequences: [582]}});
extendSprite("Tile", "cloud-happy1", {collision: false}, {idle: {sequences: [585]}});
extendSprite("Tile", "cloud-happy2", {collision: false}, {idle: {sequences: [586]}});
extendSprite("Tile", "cloud-happy3", {collision: false}, {idle: {sequences: [587]}});
extendSprite("Tile", "flag-pole1", {collision: false}, {idle: {sequences: [306]}});
extendSprite("Tile", "cloud-small", {collision: true}, {idle: {sequences: [613]}});
extendSprite("Tile", "ground-ug", {collision: true}, {idle: {sequences: [116]}});
extendSprite("Tile", "ground2-ug", {collision: true}, {idle: {sequences: [147]}});
extendSprite("Tile", "block-ug", {collision: true}, {idle: {sequences: [119]}});
extendSprite("Tile", "block2-ug", {collision: true}, {idle: {sequences: [145]}});
extendSprite("Tile", "tube3", {collision: true}, {idle: {sequences: [319]}});
extendSprite("Tile", "tube4", {collision: true}, {idle: {sequences: [320]}});
extendSprite("Tile", "tube3-mirror", {collision: true}, {idle: {sequences: [232]}});
extendSprite("Tile", "tube4-mirror", {collision: true}, {idle: {sequences: [233]}});
extendSprite("Tile", "tube3-ug", {collision: true}, {idle: {sequences: [435]}});
extendSprite("Tile", "tube4-ug", {collision: true}, {idle: {sequences: [436]}});
extendSprite("Tile", "bush4", {collision: false}, {idle: {sequences: [269]}});
extendSprite("Tile", "bush5", {collision: false}, {idle: {sequences: [270]}});
extendSprite("Tile", "bush6", {collision: false}, {idle: {sequences: [271]}});
extendSprite("Tile", "cloud4", {collision: false}, {idle: {sequences: [609]}});
extendSprite("Tile", "cloud5", {collision: false}, {idle: {sequences: [610]}});
extendSprite("Tile", "cloud6", {collision: false}, {idle: {sequences: [611]}});
extendSprite("Tile", "cloud-happy4", {collision: false}, {idle: {sequences: [614]}});
extendSprite("Tile", "cloud-happy5", {collision: false}, {idle: {sequences: [615]}});
extendSprite("Tile", "cloud-happy6", {collision: false}, {idle: {sequences: [616]}});
extendSprite("Tile", "flag-pole2", {collision: false}, {idle: {sequences: [335]}});
extendSprite("Tile", "tube1-out", {collision: true}, {idle: {sequences: [292]}});
extendSprite("Tile", "tube2-out", {collision: true}, {idle: {sequences: [293]}});
extendSprite("Tile", "tube3-out", {collision: true}, {idle: {sequences: [294]}});
extendSprite("Tile", "tube1-out-mirror", {collision: true}, {idle: {sequences: [234]}});
extendSprite("Tile", "tube2-out-mirror", {collision: true}, {idle: {sequences: [235]}});
extendSprite("Tile", "tube3-out-mirror", {collision: true}, {idle: {sequences: [236]}});
extendSprite("Tile", "tube1-out-ug", {collision: true}, {idle: {sequences: [408]}});
extendSprite("Tile", "tube2-out-ug", {collision: true}, {idle: {sequences: [409]}});
extendSprite("Tile", "tube3-out-ug", {collision: true}, {idle: {sequences: [410]}});
extendSprite("Tile", "brick7-castle", {collision: true}, {idle: {sequences: [42]}});
extendSprite("Tile", "bush7", {collision: false}, {idle: {sequences: [272]}});
extendSprite("Tile", "bush8", {collision: false}, {idle: {sequences: [273]}});
extendSprite("Tile", "bush9", {collision: false}, {idle: {sequences: [274]}});
extendSprite("Tile", "water1", {collision: false}, {idle: {sequences: [583]}});
extendSprite("Tile", "water2", {collision: false}, {idle: {sequences: [612]}});
extendSprite("Tile", "water-bridge", {collision: true}, {idle: {sequences: [32]}});
extendSprite("Tile", "lava1", {collision: false}, {idle: {sequences: [699]}});
extendSprite("Tile", "lava2", {collision: false}, {idle: {sequences: [728]}});
extendSprite("Tile", "lava-bridge", {collision: true}, {idle: {sequences: [148]}});
extendSprite("Tile", "tube4-out", {collision: true}, {idle: {sequences: [321]}});
extendSprite("Tile", "tube5-out", {collision: true}, {idle: {sequences: [322]}});
extendSprite("Tile", "tube6-out", {collision: true}, {idle: {sequences: [323]}});
extendSprite("Tile", "tube4-out-mirror", {collision: true}, {idle: {sequences: [263]}});
extendSprite("Tile", "tube5-out-mirror", {collision: true}, {idle: {sequences: [264]}});
extendSprite("Tile", "tube6-out-mirror", {collision: true}, {idle: {sequences: [265]}});
extendSprite("Tile", "tube4-out-ug", {collision: true}, {idle: {sequences: [437]}});
extendSprite("Tile", "tube5-out-ug", {collision: true}, {idle: {sequences: [438]}});
extendSprite("Tile", "tube6-out-ug", {collision: true}, {idle: {sequences: [439]}});
extendSprite("Tile", "brick-castle", {collision: true}, {idle: {sequences: [11]}});
extendSprite("Tile", "brick2-castle", {collision: true}, {idle: {sequences: [12]}});
extendSprite("Tile", "brick3-castle", {collision: true}, {idle: {sequences: [13]}});
extendSprite("Tile", "brick4-castle", {collision: true}, {idle: {sequences: [14]}});
extendSprite("Tile", "brick5-castle", {collision: true}, {idle: {sequences: [40]}});
extendSprite("Tile", "brick6-castle", {collision: true}, {idle: {sequences: [41]}});
extendSprite("Tile", "railing", {collision: false}, {idle: {sequences: [363]}});
extendSprite("Tile", "platform1", {collision: true}, {idle: {sequences: [237]}});
extendSprite("Tile", "platform2", {collision: true}, {idle: {sequences: [238]}});
extendSprite("Tile", "platform3", {collision: true}, {idle: {sequences: [239]}});
extendSprite("Tile", "platform-pole", {collision: false}, {idle: {sequences: [208]}});
// Subclass Sprite to use a global timer - so all animated
// sprites animate in sync.
Backbone.AnimatedTile = Backbone.Tile.extend({
initialize: function(attributes, options) {
Backbone.Tile.prototype.initialize.apply(this, arguments);
this.on("attach", this.onAttach, this);
this.on("detach", this.onDetach, this);
},
onAttach: function() {
if (!this.engine) return;
this.onDetach();
this.clock = this.engine.sprites.findWhere({name: "animatedTileClock"});
if (!this.clock)
this.clock = this.engine.add(new Backbone.Clock({name: "animatedTileClock", delay: 200}));
this.listenTo(this.clock, "change:ticks", this.updateAnimationIndex);
},
onDetach: function() {
if (this.clock) this.stopListening(this.clock);
this.clock = undefined;
},
update: function(dt) {
return true;
},
updateAnimationIndex: function() {
var animation = this.getAnimation(),
sequenceIndex = this.get("sequenceIndex") || 0;
if (!animation) return;
this.set("sequenceIndex", sequenceIndex < animation.sequences.length-1 ? sequenceIndex + 1 : 0);
}
});
Backbone.Brick = Backbone.AnimatedTile.extend({
defaults: _.extend({}, Backbone.Tile.prototype.defaults, {
name: "brick",
state: "idle",
collision: true,
static: false
}),
animations: {
idle: {
sequences: [2]
},
bounce: {
sequences: [
{frame: 2, x: 0, y: -8},
{frame: 2, x: 0, y: -8},
{frame: 2, x: 0, y: -4},
{frame: 2, x: 0, y: 0}
],
delay: 50
}
},
initialize: function(attributes, options) {
Backbone.AnimatedTile.prototype.initialize.apply(this, arguments);
this.on("hit", this.hit, this);
},
hit: function(sprite, dir, dir2) {
if (sprite.get("hero") && dir == "bottom") {
var tile = this;
this.set({state: "bounce", sequenceIndex: 0});
this.world.setTimeout(function() {
tile.set({state: "idle"});
}, 200);
} else if (dir == "top") {
sprite.trigger("hit", this, "bottom");
}
return this;
}
});
var NewTile = extendSprite("Brick", "brick-top");
animations = NewTile.prototype.animations;
animations.idle.sequences = [1];
_.each(animations.bounce.sequences, function(sequence) {sequence.frame = 1;});
NewTile = extendSprite("Brick", "brick-ug");
animations = NewTile.prototype.animations;
animations.idle.sequences = [118];
_.each(animations.bounce.sequences, function(sequence) {sequence.frame = 118;});
NewTile = extendSprite("Brick", "brick-top-ug");
animations = NewTile.prototype.animations;
animations.idle.sequences = [117];
_.each(animations.bounce.sequences, function(sequence) {sequence.frame = 117;});
Backbone.QuestionBlock = Backbone.AnimatedTile.extend({
defaults: _.extend({}, Backbone.Tile.prototype.defaults, {
name: "question-block",
state: "idle",
collision: true,
static: false
}),
animations: {
idle: {
sequences: [23, 23, 24, 25, 24, 23],
delay: 150
},
bounce: {
sequences: [
{frame: 3, x: 0, y: -8},
{frame: 3, x: 0, y: -8},
{frame: 3, x: 0, y: -4},
{frame: 3, x: 0, y: 0}
],
delay: 50
},
empty: {
sequences: [3]
}
},
initialize: function(attributes, options) {
Backbone.AnimatedTile.prototype.initialize.apply(this, arguments);
this.on("hit", this.hit, this);
},
hit: function(sprite, dir, dir2) {
if (!sprite || !sprite.get("hero") || dir != "bottom") return;
if (this.get("state") != "idle") return;
var tile = this;
this.set({state: "bounce", sequenceIndex: 0});
if (this.world)
this.world.add(new Backbone.FlyingPennie({
x: this.get("x"),
y: this.get("y")
}));
setTimeout(function() {
tile.set({state: "empty"});
}, 200);
}
});
NewTile = extendSprite("QuestionBlock", "question-block-ug");
animations = NewTile.prototype.animations;
animations.idle.sequences = [139, 139, 140, 141, 140, 139];
_.each(animations.bounce.sequences, function(sequence) {sequence.frame = 119;});
animations.empty.sequences = [119];
NewTile = extendSprite("QuestionBlock", "invisible-question-block");
animations = NewTile.prototype.animations;
animations.idle.sequences = [];
_.each(animations.bounce.sequences, function(sequence) {sequence.frame = 3;});
animations.empty.sequences = [3];
NewTile = extendSprite("QuestionBlock", "invisible-question-block-ug");
animations = NewTile.prototype.animations;
animations.idle.sequences = [];
_.each(animations.bounce.sequences, function(sequence) {sequence.frame = 119;});
animations.empty.sequences = [119];
}).call(this);