Merge branch 'develop' into toebu-tags-filters

Conflicts:
	src/app/index.coffee
	src/app/tasks.coffee
	styles/app/index.styl
	views/app/index.html
	views/app/tasks.html
This commit is contained in:
Tyler Renelle 2013-03-18 17:09:42 -04:00
commit f332c5d9a8
70 changed files with 4040 additions and 2488 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ node_modules
*.swp
.idea*
config.json
npm-debug.log

3
.gitmodules vendored
View file

@ -19,3 +19,6 @@
[submodule "public/vendor/BrowserQuest"]
path = public/vendor/BrowserQuest
url = https://github.com/mozilla/BrowserQuest.git
[submodule "public/vendor/bootstrap-tour"]
path = public/vendor/bootstrap-tour
url = git://github.com/sorich87/bootstrap-tour.git

2
CHANGELOG.md Normal file
View file

@ -0,0 +1,2 @@
3/3/2013
* Add custom day start: https://trello.com/card/custom-day-start/50e5d3684fe3a7266b0036d6/15

View file

@ -1,18 +1,11 @@
#[HabitRPG](http://habitrpg.com/)
[HabitRPG](http://habitrpg.com/) is a habit tracker app which treats your goals like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
HabitRPG is a habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
[Read more](https://habitrpg.com/static/about)
![Screenshot](https://raw.github.com/lefnire/habitrpg/master/public/img/screenshot.jpeg "Screenshot")
###[About](https://habitrpg.com/splash.html)
###[Running Locally](https://github.com/lefnire/habitrpg/wiki/Running-Locally)
###[API](https://github.com/lefnire/habitrpg/wiki/API)
##Contact
###[Bugs](https://github.com/lefnire/habitrpg/issues)
###[New Features](https://trello.com/board/habitrpg/50e5d3684fe3a7266b0036d6)
###[Email](mailto:tylerrenelle@gmail.com)
##License
Code is licensed under GNU GPL v3. Content is licensed under CC-BY-SA 3.0.
See the LICENSE file for details.

View file

@ -0,0 +1,39 @@
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130307_normalize_algo_values.js
/**
* Make sure people aren't overflowing their exp with the new system
*/
db.users.find().forEach(function(user){
function oldTnl(level) {
return (Math.pow(level,2)*10)+(level*10)+80
}
function newTnl(level) {
var value = 0;
if (level >= 100) {
value = 0
} else {
value = Math.round(((Math.pow(level,2)*0.25)+(10 * level) + 139.75)/10)*10; // round to nearest 10
}
return value
}
var newTnl = newTnl(user.stats.lvl);
if (user.stats.exp > newTnl) {
var percent = user.stats.exp / oldTnl(user.stats.lvl);
percent = (percent>1) ? 1 : percent;
user.stats.exp = newTnl * percent;
try {
db.users.update(
{_id:user._id},
{$set: {'stats.exp': user.stats.exp}},
{multi:true}
);
} catch(e) {
print(e);
}
}
})

View file

@ -0,0 +1,47 @@
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130307_normalize_algo_values.js
/**
* Users were experiencing a lot of extreme Exp multiplication (https://github.com/lefnire/habitrpg/issues/594).
* This sets things straight, and in preparation for another algorithm overhaul
*/
db.users.find().forEach(function(user){
if (user.stats.exp >= 3580) {
user.stats.exp = 0;
}
if (user.stats.lvl > 100) {
user.stats.lvl = 100;
}
_.each(user.tasks, function(task, key){
// remove corrupt tasks
if (!task) {
delete user.tasks[key];
return;
}
// Fix busted values
if (task.value > 21.27) {
task.value = 21.27;
}
else if (task.value < -47.27) {
task.value = -47.27;
}
});
try {
db.users.update(
{_id:user._id},
{$set:
{
'stats.lvl': user.stats.lvl,
'stats.exp': user.stats.exp,
'tasks' : user.tasks
}
},
{multi:true}
);
} catch(e) {
print(e);
}
})

View file

@ -0,0 +1,28 @@
/**
* Remove duff histories for dailies
*/
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130307_remove_duff_histories.js
db.users.find().forEach(function(user){
_.each(user.tasks, function(task, key){
if (task.type === "daily") {
// remove busted history entries
task.history = _.filter(task.history, function(h){return !!h.value})
}
});
try {
db.users.update(
{_id:user._id},
{$set:
{
'tasks' : user.tasks
}
},
{multi:true}
);
} catch(e) {
print(e);
}
})

View file

@ -0,0 +1,11 @@
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/find_unique_user.js
/**
* There are some rare instances of lost user accounts, due to a corrupt user auth variable (see https://github.com/lefnire/habitrpg/wiki/User-ID)
* Past in the text of a unique habit here to find the user, then you can restore their UUID
*/
db.users.find().forEach(function(user){
var found = _.findWhere(user.tasks, {text: "Replace Me"})
if (found) printjson({id:user._id, auth:user.auth});
})

View file

@ -4,7 +4,7 @@
"version": "0.0.0-150",
"main": "./server.js",
"dependencies": {
"derby": "git://github.com/lefnire/derby#habitrpg",
"derby": "git://github.com/Unroll-Me/derby#master",
"racer": "git://github.com/lefnire/racer#habitrpg",
"racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg",
"derby-ui-boot": "git://github.com/codeparty/derby-ui-boot#master",
@ -23,9 +23,10 @@
"mongoskin": "*",
"nconf": "*",
"icalendar": "git://github.com/lefnire/node-icalendar#master",
"superagent": "~0.12.4",
"resolve": "~0.2.3",
"browserify": "1.17.3",
"webkit-devtools-agent": "*"
"expect.js": "~0.2.0"
},
"private": true,
"subdomain": "habitrpg",
@ -38,6 +39,7 @@
"npm": "1.1.x"
},
"scripts": {
"start": "server.js"
"start": "server.js",
"test": "mocha test/api.mocha.coffee"
}
}

View file

@ -1,27 +1,17 @@
<html>
<head>
<meta charset="utf-8">
<title>HabitRPG | Gamify Your Life</title>
<title>HabitRPG | Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles -->
<link href="/vendor/bootstrap/docs/assets/css/bootstrap.css" rel="stylesheet">
<link href="/vendor/bootstrap/docs/assets/css/bootstrap-responsive.css" rel="stylesheet">
<!-- CDN -->
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap.min.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap-responsive.min.css" rel="stylesheet">
<link href="/vendor/bootstrap/docs/assets/css/docs.css" rel="stylesheet">
<link href="/css/static-pages.css" rel="stylesheet">
<style type="text/css">
.jumbotron {
background-color: #1b1b1b;
background-image: -moz-linear-gradient(top, #222222, #111111);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));
background-image: -webkit-linear-gradient(top, #222222, #111111);
background-image: -o-linear-gradient(top, #222222, #111111);
background-image: linear-gradient(to bottom, #222222, #111111);
background-repeat: repeat-x;
border-color: #252525;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);
}
.rotate-img {
transform:rotate(90deg);
-ms-transform:rotate(90deg); /* IE 9 */
@ -37,7 +27,7 @@
<div class='marketing'>
<img class='rotate-img' src="/img/sprites/armor3_m.png" />
<h2>The server is experiencing issues.</h2>
<p><a href="/">Try again</a> in a few, the developer has been notified. The most likely culprit is <a href="https://github.com/lefnire/habitrpg/issues/165">this issue</a> which Tyler is working frantically to fix. (Any memory leak experts?)</p>
<p><a href="/">Try again</a> in a few, the developer has been notified. The most likely culprit is <a href="https://github.com/lefnire/habitrpg/issues/165">this issue</a> which Tyler is working to fix. (Any memory leak experts?)</p>
</div>
</div>
</body>

76
public/css/footer.css Normal file
View file

@ -0,0 +1,76 @@
/* Sticky footer styles
-------------------------------------------------- */
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
/* Wrapper for page content to push down footer */
#wrap {
min-height: 100%;
height: auto !important;
height: 100%;
/* Negative indent footer by it's height */
margin: 0 auto -60px;
}
/* Set the fixed height of the footer here */
#push,
#footer {
height: 60px;
}
#footer {
background-color: #f5f5f5;
}
/* Lastly, apply responsive CSS fixes as necessary */
@media (max-width: 767px) {
#footer {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
#wrap > .container {
padding-top: 60px;
}
.container .credit {
margin: 20px 0;
}
code {
font-size: 80%;
}
/* HabitRPG Custom CSS
-------------------------------------------------- */
.footer {
/*padding: 70px 0;*/
border-top: 1px solid #e5e5e5;
background-color: #f5f5f5;
}
.footer li {
text-align: left;
}
.footer h4 {
text-align: left;
}
.footer td {
align: left !important;
}

View file

@ -0,0 +1,17 @@
.jumbotron {
background-color: #1b1b1b;
background-image: -moz-linear-gradient(top, #222222, #111111);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));
background-image: -webkit-linear-gradient(top, #222222, #111111);
background-image: -o-linear-gradient(top, #222222, #111111);
background-image: linear-gradient(to bottom, #222222, #111111);
background-repeat: repeat-x;
border-color: #252525;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);
background:none;
color:black;
}
body {
padding-top:0px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,17 @@
#PetEggs {
/* egg sprite size */
width: 48px;
height: 51px;
/* link to sprite sheet image */
background-image: url(Egg_Sprite_Sheet.png);
}
/* defines where in the sheet the eggs are */
.WolfEgg{background-position: 0px 0px;}
.TigerEgg{background-position: 0px -51px;}
.PolarBearEgg{background-position: 0px -102px;}
.PandaEgg{background-position: 0px -153px;}
.LionEgg{background-position: 0px -204px;}
.FlyingPigEgg{background-position: 0px -255px;}
.DrakeEgg{background-position: 0px -306px;}
.CactusEgg{background-position: 0px -357px;}
.BearEgg{background-position: 0px -408px;}

View file

@ -1,122 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>HabitRPG | Gamify Your Life</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles -->
<link href="/vendor/bootstrap/docs/assets/css/bootstrap.css" rel="stylesheet">
<link href="/vendor/bootstrap/docs/assets/css/bootstrap-responsive.css" rel="stylesheet">
<link href="/vendor/bootstrap/docs/assets/css/docs.css" rel="stylesheet">
<script type="text/javascript" src="vendor/jquery-ui/jquery-1.9.1.js"></script>
<script type="text/javascript">
$.getScript("https://s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire");
</script>
<style type="text/css">
.jumbotron {
background-color: #1b1b1b;
background-image: -moz-linear-gradient(top, #222222, #111111);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));
background-image: -webkit-linear-gradient(top, #222222, #111111);
background-image: -o-linear-gradient(top, #222222, #111111);
background-image: linear-gradient(to bottom, #222222, #111111);
background-repeat: repeat-x;
border-color: #252525;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);
background:none;
color:black;
}
body {
padding-top:0px;
}
</style>
</head>
<body>
<div class='jumbotron masthead'>
<div class='container'>
<h1><img src="/img/logo/habitrpg_pixel.png" alt="HabitRPG"/></h1>
<p>Habit tracking which treats your goals like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.</p>
<a href="/?play=1" class='btn btn-primary btn-small'>Play</a>
</div>
</div>
<div class="bs-docs-social">
<div class="container">
<ul class="bs-docs-social-buttons">
<li>
<table>
<tr>
<td><!-- Github -->
<iframe src="/vendor/github-buttons/github-btn.html?user=lefnire&repo=habitrpg&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="85px" height="20px"></iframe>
</td>
<td>
<div class="addthis_toolbox addthis_default_style "
addthis:url="https://habitrpg.com"
addthis:title="HabitRPG: Role Playing Game for Self Improvement">
<a class="addthis_button_facebook_like" fb:like:layout="button_count"></a>
<a class="addthis_button_tweet" tw:via="habitrpg"></a>
</div>
</td>
<td><a href="http://habitrpg.tumblr.com/">Blog</a>&nbsp;-&nbsp;</td>
<td><a href="https://github.com/lefnire/habitrpg/wiki/FAQ">FAQ</a>&nbsp;-&nbsp;</td>
<td><a href="https://github.com/lefnire/habitrpg/wiki/API">API</a>&nbsp;-&nbsp;</td>
<td><a href="https://github.com/lefnire/habitrpg#contact">Contact</a></td>
</tr>
</table>
</li>
</ul>
</div>
</div>
<div class='container'>
<div class='marketing'>
<p>&nbsp;</p>
<p><iframe src="http://player.vimeo.com/video/57639356" width="960" height="539" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe></p>
</div>
<section>
<div class="page-header">
<h1>Habits</h1>
</div>
<p class="lead">Habits are goals that you constantly track. For some habits, it only makes sense to gain points (eg, "1h Productive Work"). For others, it only makes sense to lose points (like "Eat Junk Food"). For the rest, both gain and loss apply (eg, for "Take The Stairs", stairs is a gain, elevator is a loss).</p>
</section>
<section>
<div class="page-header">
<h1>Dailies</h1>
</div>
<p class="lead">Dailies are goals that you want to complete once a day. At the end of each day, non-completed Dailies dock you points. If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit. But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.</p>
</section>
<section>
<div class="page-header">
<h1>Todos</h1>
</div>
<p class="lead">Todos are one-off goals which need to be completed eventually. Non-completed Todos wont hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos.</p>
</section>
<section>
<div class="page-header">
<h1>Rewards</h1>
</div>
<p class='lead'>As you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits. But only buy if you have enough gold - you lose HP otherwise.</p>
<p class='lead'>After youve played for a while, you unlock the <strong>Item Store</strong> under the rewards column. You can now buy weapons, armor, potions, etc. Armor decreases HP loss (by an increasing amount wich each upgrade). Weapons increase experience gain. Potions recover 15 HP.</p>
</section>
<section>
<div class="page-header">
<h1>License & Credits</h1>
</div>
<p class='lead'>Code is licensed under GNU GPL v3. Content is licensed under CC-BY-SA 3.0. See the LICENSE file for details.</p>
<p class='lead'>Content and assets comes from Mozillas <a href="http://browserquest.mozilla.org/">BrowserQuest</a> (<a href="http://mozilla.org">Mozilla</a>, <a href="http://www.littleworkshop.fr">Little Workshop</a>). They seriously rock.
</p>
</section>
</div>
</body>
</html>

@ -1 +1 @@
Subproject commit 8c7f9c66a7d12f47f50618ef420868fe836d0c33
Subproject commit eb24718add4dd36fe92fdbdb79e6ff4ce5919300

1
public/vendor/bootstrap-tour vendored Submodule

@ -0,0 +1 @@
Subproject commit 4f94fa056c88c6099dea135b644d8f95d38ac9e1

View file

@ -1,271 +0,0 @@
/* ============================================================
# bootstrap-tour.js v0.1
# http://pushly.github.com/bootstrap-tour/
# ==============================================================
# Copyright 2012 Push.ly
#
# 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.
*/
(function() {
(function($, window) {
var Tour, document;
document = window.document;
Tour = (function() {
function Tour(options) {
var _this = this;
this._options = $.extend({
name: 'tour',
labels: {
end: 'End tour',
next: 'Next &raquo;',
prev: '&laquo; Prev'
},
keyboard: true,
afterSetState: function(key, value) {},
afterGetState: function(key, value) {},
onShow: function(tour) {},
onHide: function(tour) {},
onShown: function(tour) {}
}, options);
this._steps = [];
this.setCurrentStep();
this._onresize(function() {
if (!_this.ended) return _this.showStep(_this._current);
});
}
Tour.prototype.setState = function(key, value) {
$.cookie("" + this._options.name + "_" + key, value, {
expires: 36500,
path: '/'
});
return this._options.afterSetState(key, value);
};
Tour.prototype.getState = function(key) {
var value;
value = $.cookie("" + this._options.name + "_" + key);
this._options.afterGetState(key, value);
return value;
};
Tour.prototype.addStep = function(step) {
return this._steps.push(step);
};
Tour.prototype.getStep = function(i) {
if (this._steps[i] != null) {
return $.extend({
path: "",
placement: "right",
title: "",
content: "",
next: i === this._steps.length - 1 ? -1 : i + 1,
prev: i - 1,
animation: true,
onShow: this._options.onShow,
onHide: this._options.onHide,
onShown: this._options.onShown
}, this._steps[i]);
}
};
Tour.prototype.start = function(force) {
var _this = this;
if (force == null) force = false;
if (this.ended() && !force) return;
$(document).off("click.bootstrap-tour", ".popover .next").on("click.bootstrap-tour", ".popover .next", function(e) {
e.preventDefault();
return _this.next();
});
$(document).off("click.bootstrap-tour", ".popover .prev").on("click.bootstrap-tour", ".popover .prev", function(e) {
e.preventDefault();
return _this.prev();
});
$(document).off("click.bootstrap-tour", ".popover .end").on("click.bootstrap-tour", ".popover .end", function(e) {
e.preventDefault();
return _this.end();
});
this._setupKeyboardNavigation();
return this.showStep(this._current);
};
Tour.prototype.next = function() {
this.hideStep(this._current);
return this.showNextStep();
};
Tour.prototype.prev = function() {
this.hideStep(this._current);
return this.showPrevStep();
};
Tour.prototype.end = function() {
this.hideStep(this._current);
$(document).off(".bootstrap-tour");
return this.setState("end", "yes");
};
Tour.prototype.ended = function() {
return !!this.getState("end");
};
Tour.prototype.restart = function() {
this.setState("current_step", null);
this.setState("end", null);
this.setCurrentStep(0);
return this.start();
};
Tour.prototype.hideStep = function(i) {
var step;
step = this.getStep(i);
if (step.onHide != null) step.onHide(this);
return $(step.element).popover("hide");
};
Tour.prototype.showStep = function(i) {
var step;
step = this.getStep(i);
if (!step) return;
this.setCurrentStep(i);
if (step.path !== "" && document.location.pathname !== step.path && document.location.pathname.replace(/^.*[\\\/]/, '') !== step.path) {
document.location.href = step.path;
return;
}
if (step.onShow != null) step.onShow(this);
if (!((step.element != null) && $(step.element).length !== 0 && $(step.element).is(":visible"))) {
this.showNextStep();
return;
}
this._showPopover(step, i);
if (step.onShown != null) return step.onShown(this);
};
Tour.prototype.setCurrentStep = function(value) {
if (value != null) {
this._current = value;
return this.setState("current_step", value);
} else {
this._current = this.getState("current_step");
if (this._current === null || this._current === "null") {
return this._current = 0;
} else {
return this._current = parseInt(this._current);
}
}
};
Tour.prototype.showNextStep = function() {
var step;
step = this.getStep(this._current);
return this.showStep(step.next);
};
Tour.prototype.showPrevStep = function() {
var step;
step = this.getStep(this._current);
return this.showStep(step.prev);
};
Tour.prototype._showPopover = function(step, i) {
var content, nav, options, tip,
_this = this;
content = "" + step.content + "<br /><p>";
options = $.extend({}, this._options);
if (step.options) $.extend(options, step.options);
if (step.reflex) {
$(step.element).css("cursor", "pointer");
$(step.element).on("click", function(e) {
$(step.element).css("cursor", "auto");
return _this.next();
});
}
nav = [];
if (step.prev >= 0) {
nav.push("<a href='#" + step.prev + "' class='prev'>" + options.labels.prev + "</a>");
}
if (step.next >= 0) {
nav.push("<a href='#" + step.next + "' class='next'>" + options.labels.next + "</a>");
}
content += nav.join(" | ");
content += "<a href='#' class='pull-right end'>" + options.labels.end + "</a>";
$(step.element).popover({
placement: step.placement,
trigger: "manual",
title: step.title,
content: content,
html: true,
animation: step.animation
}).popover("show");
tip = $(step.element).data("popover").tip();
this._reposition(tip);
return this._scrollIntoView(tip);
};
Tour.prototype._reposition = function(tip) {
var offsetBottom, offsetRight, tipOffset;
tipOffset = tip.offset();
offsetBottom = $(document).outerHeight() - tipOffset.top - $(tip).outerHeight();
if (offsetBottom < 0) tipOffset.top = tipOffset.top + offsetBottom;
offsetRight = $(document).outerWidth() - tipOffset.left - $(tip).outerWidth();
if (offsetRight < 0) tipOffset.left = tipOffset.left + offsetRight;
if (tipOffset.top < 0) tipOffset.top = 0;
if (tipOffset.left < 0) tipOffset.left = 0;
return tip.offset(tipOffset);
};
Tour.prototype._scrollIntoView = function(tip) {
var tipRect;
tipRect = tip.get(0).getBoundingClientRect();
if (!(tipRect.top > 0 && tipRect.bottom < $(window).height() && tipRect.left > 0 && tipRect.right < $(window).width())) {
return tip.get(0).scrollIntoView(true);
}
};
Tour.prototype._onresize = function(cb, timeout) {
return $(window).resize(function() {
clearTimeout(timeout);
return timeout = setTimeout(cb, 100);
});
};
Tour.prototype._setupKeyboardNavigation = function() {
var _this = this;
if (this._options.keyboard) {
return $(document).on("keyup.bootstrap-tour", function(e) {
if (!e.which) return;
switch (e.which) {
case 39:
e.preventDefault();
if (_this._current < _this._steps.length - 1) return _this.next();
break;
case 37:
e.preventDefault();
if (_this._current > 0) return _this.prev();
}
});
}
};
return Tour;
})();
return window.Tour = Tour;
})(jQuery, window);
}).call(this);

View file

@ -22,7 +22,7 @@ process.env.SMTP_SERVICE = conf.get("SMTP_SERVICE");
process.env.STRIPE_API_KEY = conf.get("STRIPE_API_KEY");
process.env.STRIPE_PUB_KEY = conf.get("STRIPE_PUB_KEY");
var agent;
/*var agent;
if (process.env.NODE_ENV === 'development') {
// Follow these instructions for profiling / debugging leaks
// * https://developers.google.com/chrome-developer-tools/docs/heap-profiling
@ -31,7 +31,7 @@ if (process.env.NODE_ENV === 'development') {
console.log("To debug memory leaks:" +
"\n\t(1) Run `kill -SIGUSR2 " + process.pid + "`" +
"\n\t(2) open http://c4milo.github.com/node-webkit-agent/21.0.1180.57/inspector.html?host=localhost:1337&page=0");
}
}*/
process.on('uncaughtException', function (error) {

67
src/app/algos.coffee Normal file
View file

@ -0,0 +1,67 @@
XP = 15
HP = 2
priorityValue = (priority='!') ->
switch priority
when '!' then 1
when '!!' then 1.5
when '!!!' then 2
else 1
module.exports.tnl = (level) ->
if level >= 100
value = 0
else
value = Math.round(((Math.pow(level,2)*0.25)+(10 * level) + 139.75)/10)*10 # round to nearest 10
return value
###
Calculates Exp modificaiton based on level and weapon strength
{value} task.value for exp gain
{weaponStrength) weapon strength
{level} current user level
{priority} user-defined priority multiplier
###
module.exports.expModifier = (value, weaponStr, level, priority='!') ->
str = (level-1) / 2 # ultimately get this from user
totalStr = (str + weaponStr) / 100
strMod = 1 + totalStr
exp = value * XP * strMod * priorityValue(priority)
return Math.round(exp)
###
Calculates HP modification based on level and armor defence
{value} task.value for hp loss
{armorDefense} defense from armor
{helmDefense} defense from helm
{level} current user level
{priority} user-defined priority multiplier
###
module.exports.hpModifier = (value, armorDef, helmDef, shieldDef, level, priority='!') ->
def = (level-1) / 2 # ultimately get this from user?
totalDef = (def + armorDef + helmDef + shieldDef) / 100 #ultimate get this from user
defMod = 1 - totalDef
hp = value * HP * defMod * priorityValue(priority)
return Math.round(hp * 10)/10 # round to 1dp
###
Future use
{priority} user-defined priority multiplier
###
module.exports.gpModifier = (value, modifier, priority='!') ->
return value * modifier * priorityValue(priority)
###
Calculates the next task.value based on direction
Uses a capped inverse log y=.95^x, y>= -5
{currentValue} the current value of the task
{direction} up or down
###
module.exports.taskDeltaFormula = (currentValue, direction) ->
if currentValue < -47.27 then currentValue = -47.27
else if currentValue > 21.27 then currentValue = 21.27
delta = Math.pow(0.9747,currentValue)
return delta if direction is 'up'
return -delta

View file

@ -1,75 +1,36 @@
_ = require 'underscore'
moment = require 'moment'
#algos = require './algos'
module.exports.restoreRefs = restoreRefs = (model) ->
# tnl function
model.fn '_tnl', '_user.stats.lvl', (lvl) ->
# see https://github.com/lefnire/habitrpg/issues/4
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
restoreRefs = module.exports.restoreRefs = (model) ->
#refLists
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
module.exports.resetDom = (model) ->
window.DERBY.app.dom.clear()
restoreRefs(model)
window.DERBY.app.view.render(model)
reconstructPage model
module.exports.app = (appExports, model) ->
reconstructPage model
reconstructPage = (model) ->
loadJavaScripts(model)
setupSortable(model)
setupTooltips(model)
setupTour(model)
setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
$('.datepicker').datepicker({autoclose:true, todayBtn:true})
.on 'changeDate', (ev) ->
#for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved
#FIXME also, it saves as a day behind??
model.at(ev.target).set 'date', moment(ev.date).add('d',1).format('MM/DD/YYYY')
###
Loads JavaScript files from (1) public/js/* and (2) external sources
We use this file (instead of <Scripts:> or <Tail:> inside .html) so we can utilize require() to concatinate for
faster page load, and $.getScript for asyncronous external script loading
If a library is available in a CDN, we put it in <Scripts:> (index.html) for better caching. If not, we use
this function to utilize require() to concatinate for faster page load, and $.getScript for asyncronous external script loading
###
loadJavaScripts = (model) ->
require '../../public/vendor/jquery-ui/jquery-1.9.1'
unless model.get('_view.mobileDevice')
require '../../public/vendor/jquery-ui/ui/jquery.ui.core'
require '../../public/vendor/jquery-ui/ui/jquery.ui.widget'
require '../../public/vendor/jquery-ui/ui/jquery.ui.mouse'
require '../../public/vendor/jquery-ui/ui/jquery.ui.sortable'
# Bootstrap
require '../../public/vendor/bootstrap/docs/assets/js/bootstrap'
# require '../../public/vendor/bootstrap/js/bootstrap-tooltip'
# require '../../public/vendor/bootstrap/js/bootstrap-tab'
# require '../../public/vendor/bootstrap/js/bootstrap-popover'
# require '../../public/vendor/bootstrap/js/bootstrap-modal'
# require '../../public/vendor/bootstrap/js/bootstrap-dropdown'
require '../../public/vendor/jquery-cookie/jquery.cookie'
require '../../public/vendor/bootstrap-tour' #https://raw.github.com/pushly/bootstrap-tour/master/bootstrap-tour.js
require '../../public/vendor/bootstrap-datepicker/js/bootstrap-datepicker'
require '../../public/vendor/bootstrap-growl/jquery.bootstrap-growl.min'
require '../../public/vendor/bootstrap-tour/bootstrap-tour'
# JS files not needed right away (google charts) or entirely optional (analytics)
# Each file getsload asyncronously via $.getScript, so it doesn't bog page-load
unless model.get('_view.mobileDevice')
$.getScript("https://s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire");
$.getScript("//s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire");
# Google Charts
$.getScript "https://www.google.com/jsapi", ->
$.getScript "//www.google.com/jsapi", ->
# Specifying callback in options param is vital! Otherwise you get blank screen, see http://stackoverflow.com/a/12200566/362790
google.load "visualization", "1", {packages:["corechart"], callback: ->}
@ -81,15 +42,11 @@ loadJavaScripts = (model) ->
###
setupSortable = (model) ->
unless (model.get('_view.mobileDevice') == true) #don't do sortable on mobile
# Make the lists draggable using jQuery UI
# Note, have to setup helper function here and call it for each type later
# due to variable binding of "type"
setupSortable = (type) ->
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
$("ul.#{type}s").sortable
dropOnEmpty: false
cursor: "move"
items: "li"
opacity: 0.4
scroll: true
axis: 'y'
update: (e, ui) ->
@ -102,16 +59,15 @@ setupSortable = (model) ->
# Also, note that refList index arguments can either be an index
# or the item's id property
model.at("_#{type}List").pass(ignore: domId).move {id}, to
_.each ['habit', 'daily', 'todo', 'reward'], (type) -> setupSortable(type)
setupTooltips = (model) ->
$('[rel=tooltip]').tooltip()
$('[rel=popover]').popover()
# FIXME: this isn't very efficient, do model.on set for specific attrs for popover
model.on 'set', '*', ->
$('[rel=tooltip]').tooltip()
$('[rel=popover]').popover()
$('.priority-multiplier-help').popover
title: "How difficult is this task?"
trigger: "hover"
content: "This multiplies its point value. Use sparingly, rely instead on our organic value-adjustment algorithms. But some tasks are grossly more valuable (Write Thesis vs Floss Teeth). Click for more info."
setupTour = (model) ->
tourSteps = [
@ -160,12 +116,8 @@ setupTour = (model) ->
$('.main-avatar').popover('destroy') #remove previous popovers
tour = new Tour()
_.each tourSteps, (step) ->
tour.addStep
html: true
element: step.element
title: step.title
content: step.content
placement: step.placement
tour.addStep _.defaults step, {html:true}
tour._current = 0 if isNaN(tour._current) #bootstrap-tour bug
tour.start()
###
@ -180,7 +132,7 @@ setupGrowlNotifications = (model) ->
return if user.get('stats.lvl') == 0
$.bootstrapGrowl html,
ele: '#notification-area',
type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl')
type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death')
top_offset: 20
align: 'right' # ('left', 'right', or 'center')
width: 250 # (integer, or 'auto')
@ -193,21 +145,55 @@ setupGrowlNotifications = (model) ->
num = captures - args
rounded = Math.abs(num.toFixed(1))
if num < 0
statsNotification "<i class='icon-heart'></i> -#{rounded} HP", 'hp' # lost hp from purchase
statsNotification "<i class='icon-heart'></i> - #{rounded} HP", 'hp' # lost hp from purchase
else if num > 0
statsNotification "<i class='icon-heart'></i> + #{rounded} HP", 'hp' # gained hp from potion/level?
user.on 'set', 'stats.exp', (captures, args, isLocal, silent=false) ->
# unless silent
num = captures - args
rounded = Math.abs(num.toFixed(1))
if num < 0 and num > -50 # TODO fix hackey negative notification supress
statsNotification "<i class='icon-star'></i> - #{rounded} XP", 'xp'
else if num > 0
statsNotification "<i class='icon-star'></i> + #{rounded} XP", 'xp'
user.on 'set', 'stats.gp', (captures, args) ->
num = captures - args
rounded = Math.abs(num.toFixed(1))
# made purchase
if num < 0
# FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too
statsNotification "<i class='icon-star'></i> -#{rounded} GP", 'gp'
# gained gp (and thereby exp)
else if num > 0
num = Math.abs(num)
statsNotification "<i class='icon-star'></i> +#{rounded} XP", 'xp'
statsNotification "<i class='icon-gp'></i> +#{rounded} GP", 'gp'
absolute = Math.abs(num)
gold = Math.floor(absolute)
silver = Math.floor((absolute-gold)*100)
sign = if num < 0 then '-' else '+'
if gold and silver > 0
statsNotification "#{sign} #{gold} <i class='icon-gold'></i> #{silver} <i class='icon-silver'></i>", 'gp'
else if gold > 0
statsNotification "#{sign} #{gold} <i class='icon-gold'></i>", 'gp'
else if silver > 0
statsNotification "#{sign} #{silver} <i class='icon-silver'></i>", 'gp'
user.on 'set', 'stats.lvl', (captures, args) ->
if captures > args
statsNotification('<i class="icon-chevron-up"></i> Level Up!', 'lvl')
if captures is 1 and args is 0
statsNotification '<i class="icon-death"></i> You died! Game over.', 'death'
else
statsNotification '<i class="icon-chevron-up"></i> Level Up!', 'lvl'
module.exports.resetDom = (model) ->
DERBY.app.dom.clear()
DERBY.app.view.render(model, DERBY.app.view._lastRender.ns, DERBY.app.view._lastRender.context);
module.exports.app = (appExports, model, app) ->
loadJavaScripts(model)
setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
app.on 'render', (ctx) ->
#restoreRefs(model)
setupSortable(model)
setupTooltips(model)
setupTour(model)
$('.datepicker').datepicker({autoclose:true, todayBtn:true})
.on 'changeDate', (ev) ->
#for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved
#FIXME also, it saves as a day behind??
model.at(ev.target).set 'date', moment(ev.date).add('d',1).format('MM/DD/YYYY')

View file

@ -1,6 +1,7 @@
character = require './character'
browser = require './browser'
items = require './items'
algos = require './algos'
moment = require 'moment'
_ = require 'underscore'
@ -21,6 +22,9 @@ module.exports.username = username = (auth) ->
module.exports.view = (view) ->
view.fn "username", (auth) -> username(auth)
view.fn "tnl", algos.tnl
module.exports.app = (appExports, model) ->
user = model.at '_user'
@ -57,8 +61,14 @@ module.exports.app = (appExports, model) ->
batch.commit()
browser.resetDom(model)
appExports.closeCelebrationNofitication = (e, el) ->
user.set('flags.celebrationEvent', 'hide')
appExports.closeAlgosNotification = (e, el) ->
user.set('flags.algosNotification', 'hide')
appExports.closeOnliesNotification = (e, el) ->
user.set('flags.onliesNotification', 'hide')
appExports.closePriorityNotification = (e, el) ->
user.set('flags.priorityNotification', 'hide')
appExports.customizeGender = (e, el) ->
user.set 'preferences.gender', $(el).attr('data-value')
@ -76,9 +86,16 @@ module.exports.app = (appExports, model) ->
batch = new BatchUpdate(model)
batch.startTransaction()
$('#restore-form input').each ->
batch.set $(this).attr('data-for'), parseInt($(this).val())
batch.set $(this).attr('data-for'), parseInt($(this).val() || 1)
batch.commit()
appExports.toggleHeader = (e, el) ->
user.set 'preferences.hideHeader', !user.get('preferences.hideHeader')
appExports.deleteAccount = (e, el) ->
model.del "users.#{user.get('id')}", ->
window.location.href = "/logout"
user.on 'set', 'flags.customizationsNotification', (captures, args) ->
return unless captures == true
$('.main-avatar').popover('destroy') #remove previous popovers
@ -99,7 +116,7 @@ userSchema =
stats: { gp: 0, exp: 0, lvl: 1, hp: 50 }
party: { current: null, invitation: null }
items: { weapon: 0, armor: 0, head: 0, shield: 0 }
preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1' }
preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0, showHelm: true }
habitIds: []
dailyIds: []
todoIds: []
@ -119,14 +136,18 @@ module.exports.newUserObject = ->
newUser = lodash.cloneDeep userSchema
newUser.apiToken = derby.uuid()
repeat = {m:true,t:true,w:true,th:true,f:true,s:true,su:true}
defaultTasks = [
{type: 'habit', text: '1h Productive Work', notes: '-- Habits: Constantly Track --\nFor some habits, it only makes sense to *gain* points (like this one).', value: 0, up: true, down: false }
{type: 'habit', text: 'Eat Junk Food', notes: 'For others, it only makes sense to *lose* points', value: 0, up: false, down: true}
{type: 'habit', text: 'Take The Stairs', notes: 'For the rest, both + and - make sense (stairs = gain, elevator = lose)', value: 0, up: true, down: true}
{type: 'daily', text: '1h Personal Project', notes: '-- Dailies: Complete Once a Day --\nAt the end of each day, non-completed Dailies dock you points.', value: 0, completed: false }
{type: 'daily', text: 'Exercise', notes: "If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit.", value: 3, completed: false }
{type: 'daily', text: '45m Reading', notes: 'But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.', value: -10, completed: false }
{type: 'daily', text: '1h Personal Project', notes: '-- Dailies: Complete Once a Day --\nAt the end of each day, non-completed Dailies dock you points.', value: 0, completed: false, repeat: repeat }
{type: 'daily', text: 'Exercise', notes: "If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit.", value: 3, completed: false, repeat: repeat }
{type: 'daily', text: '45m Reading', notes: 'But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.', value: -10, completed: false, repeat: repeat }
{type: 'todo', text: 'Call Mom', notes: "-- Todos: Complete Eventually --\nNon-completed Todos won't hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos.", value: -3, completed: false }
{type: 'reward', text: '1 Episode of Game of Thrones', notes: '-- Rewards: Treat Yourself! --\nAs you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits.', value: 20 }
{type: 'reward', text: 'Cake', notes: 'But only buy if you have enough gold - you lose HP otherwise.', value: 10 }
]
@ -177,7 +198,7 @@ module.exports.updateUser = (model) ->
union = _.union obj[type + 'Ids'], taskIds
# 2. remove empty (grey) tasks
preened = _.filter(union, (val) -> _.contains(taskIds, val))
preened = _.filter union, (val) -> _.contains(taskIds, val) and val?
# There were indeed issues found, set the new list
batch.set("#{type}Ids", preened) # if _.difference(preened, userObj[path]).length != 0
@ -233,6 +254,7 @@ module.exports.BatchUpdate = BatchUpdate = (model) ->
commit: ->
model._dontPersist = false
# some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js
# pass true if we have levelled to supress xp notification
user.set "update__", updates
transactionInProgress = false
updates = {}

View file

@ -1,4 +1,5 @@
moment = require 'moment'
algos = require './algos'
module.exports.app = (appExports, model) ->
user = model.at('_user')
@ -8,6 +9,20 @@ module.exports.app = (appExports, model) ->
user.set 'lastCron', yesterday
window.location.reload()
appExports.emulateTenDays = ->
yesterday = +moment().subtract('days', 10).toDate()
user.set 'lastCron', yesterday
window.location.reload()
appExports.cheat = ->
user.incr 'stats.exp', 20
user.incr 'stats.exp', algos.tnl(user.get('stats.lvl'))
user.incr 'stats.gp', 1000
appExports.reset = ->
user.set 'stats.exp', 0
user.set 'stats.lvl', 0
user.set 'stats.gp', 0
user.set 'items.weapon', 0
user.set 'items.armor', 0
user.set 'items.head', 0
user.set 'items.shield', 0

View file

@ -1,8 +1,11 @@
moment = require('moment')
moment = require 'moment'
_ = require 'underscore'
# Absolute diff between two dates, based on 12am for both days
module.exports.daysBetween = (a, b) ->
Math.abs(moment(a).startOf('day').diff(moment(b).startOf('day'), 'days'))
# Absolute diff between two dates
module.exports.daysBetween = (yesterday, now, dayStart) ->
#sanity-check reset-time (is it 24h time?)
dayStart = 0 unless (dayStart? and (dayStart = parseInt(dayStart)) and dayStart >= 0 and dayStart <= 24)
Math.abs moment(yesterday).startOf('day').add('h', dayStart).diff(moment(now), 'days')
module.exports.dayMapping = dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'}
@ -15,6 +18,12 @@ module.exports.viewHelpers = (view) ->
view.fn "round", (num) ->
Math.round num
view.fn "floor", (num) ->
Math.floor num
view.fn "ceil", (num) ->
Math.ceil num
view.fn "lt", (a, b) ->
a < b
view.fn 'gt', (a, b) -> a > b
@ -29,4 +38,8 @@ module.exports.viewHelpers = (view) ->
loc = window?.location.host or process.env.BASE_URL
encodeURIComponent "http://#{loc}/v1/users/#{uid}/calendar.ics?apiToken=#{apiToken}"
view.fn "notEqual", (a, b) -> (a != b)
view.fn "and", -> _.reduce arguments, (cumm, curr) -> cumm && curr
view.fn "or", -> _.reduce arguments, (cumm, curr) -> cumm || curr

View file

@ -1,6 +1,7 @@
derby = require 'derby'
{get, view, ready} = derby.createApp module
derby.use require('derby-ui-boot'), {styles: ['bootstrap', 'responsive']}
app = derby.createApp module
{get, view, ready} = app
derby.use require('derby-ui-boot'), {styles: []}
derby.use require '../../ui'
derby.use require 'derby-auth/components'
@ -46,13 +47,12 @@ get '/', (page, model, params, next) ->
ready (model) ->
user = model.at('_user')
score = new scoring.Scoring(model)
#set cron immediately
lastCron = user.get('lastCron')
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
score.cron()
scoring.cron(model)
character.app(exports, model)
tasks.app(exports, model)
@ -61,6 +61,6 @@ ready (model) ->
profile.app(exports, model)
require('../server/private').app(exports, model)
require('./debug').app(exports, model) if model.get('_view.nodeEnv') != 'production'
browser.app(exports, model)
filters.app(exports, model)
browser.app(exports, model, app)

View file

@ -2,40 +2,40 @@ _ = require 'underscore'
items = module.exports.items =
weapon: [
{index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', modifier: 0.00, value:0}
{index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', modifier: 0.03, value:20}
{index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', modifier: 0.06, value:30}
{index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', modifier: 0.09, value:45}
{index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', modifier: 0.12, value:65}
{index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', modifier: 0.15, value:90}
{index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', modifier: 0.18, value:120}
{index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', strength: 0, value:0}
{index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', strength: 3, value:20}
{index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', strength: 6, value:30}
{index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', strength: 9, value:45}
{index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', strength: 12, value:65}
{index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', strength: 15, value:90}
{index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', strength: 18, value:120}
]
armor: [
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', modifier: 0.00, value:0}
{index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:30}
{index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', modifier: 0.06, value:45}
{index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', modifier: 0.07, value:65}
{index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', modifier: 0.08, value:90}
{index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', modifier: 0.1, value:120}
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', defense: 0, value:0}
{index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', defense: 4, value:30}
{index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', defense: 6, value:45}
{index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', defense: 7, value:65}
{index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
{index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', defense: 10, value:120}
]
head: [
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', modifier: 0.00, value:0}
{index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', modifier: 0.02, value:15}
{index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', modifier: 0.03, value:25}
{index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:45}
{index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', modifier: 0.05, value:60}
{index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', modifier: 0.06, value:80}
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', defense: 0, value:0}
{index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', defense: 2, value:15}
{index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', defense: 3, value:25}
{index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', defense: 4, value:45}
{index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', defense: 5, value:60}
{index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', defense: 6, value:80}
]
shield: [
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', modifier: 0.00, value:0}
{index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', modifier: 0.03, value:20}
{index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:35}
{index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', modifier: 0.05, value:55}
{index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', modifier: 0.07, value:70}
{index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', modifier: 0.08, value:90}
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', defense: 0, value:0}
{index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', defense: 3, value:20}
{index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', defense: 4, value:35}
{index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', defense: 5, value:55}
{index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', defense: 7, value:70}
{index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
]
potion: {type: 'potion', text: "Potion", notes: "Recover 15 HP", value: 25, classes: 'potion'}
reroll: {type: 'reroll', text: "Re-Roll", classes: 'reroll', notes: "Resets your tasks. When you're struggling and everything's red, use for a clean slate.", value:0 }
reroll: {type: 'reroll', text: "Re-Roll", classes: 'reroll', notes: "Resets your task values back to 0 (yellow). Useful when everything's red and it's hard to stay alive.", value:0 }
pets: [
{index: 0, text: 'Bear Cub', name: 'bearcub', icon: 'Pet-BearCub-Base.png', value: 3}
@ -165,22 +165,22 @@ module.exports.app = (appExports, model) ->
model.set '_view.activeTabPets', true
model.set '_view.activeTabRewards', false
user.on 'set', 'flags.itemsEnabled', (captures, args) ->
return unless captures == true
model.on 'set', '_user.flags.itemsEnabled', (captures, args) ->
return unless captures is true
html = """
<div class='item-store-popover'>
<img src='/vendor/BrowserQuest/client/img/1chest.png' />
<img src='/vendor/BrowserQuest/client/img/1/chest.png' />
Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information.
<a href='#' onClick="$('ul.items').popover('hide');return false;">[Close]</a>
<a href='#' onClick="$('div.rewards').popover('hide');return false;">[Close]</a>
</div>
"""
$('ul.items').popover
$('div.rewards').popover({
title: "Item Store Unlocked"
placement: 'left'
trigger: 'manual'
html: true
content: html
$('ul.items').popover 'show'
}).popover 'show'
user.on 'set', 'flags.petsEnabled', (captures, args) ->
return unless captures == true

View file

@ -71,8 +71,7 @@ module.exports.partySubscribe = partySubscribe = (page, model, params, next, cb)
module.exports.app = (appExports, model) ->
user = model.at('_user')
user.on 'set', 'flags.partyEnabled', (captures, args) ->
return unless captures == true
unlockPartiesNotification = ->
$('.main-avatar').popover('destroy') #remove previous popovers
html = """
<div class='party-system-popover'>
@ -89,6 +88,14 @@ module.exports.app = (appExports, model) ->
content: html
$('.main-avatar').popover 'show'
appExports.manuallyUnlockParties = ->
$("#settings-modal").modal("hide")
user.set('flags.partyEnabled', true)
unlockPartiesNotification()
user.on 'set', 'flags.partyEnabled', (captures, args) ->
unlockPartiesNotification() if captures == true
#TODO implement this when we have unsubscribe working properly
model.on 'set', '_user.party.invitation', (after, before) ->
if !before? and after? # they just got invited

View file

@ -20,6 +20,6 @@ module.exports.app = (appExports, model) ->
appExports.profileChangeActive = (e, el) ->
uid = $(el).attr('data-uid')
model.ref '_profileActive', model.at("users.#{uid}")
model.set '_profileActiveMain', model.get('_user.id') == uid
model.set '_profileActiveMain', user.get('id') is uid
model.set '_profileActiveUsername', character.username model.get('_profileActive.auth')

View file

@ -5,269 +5,261 @@ helpers = require './helpers'
browser = require './browser'
character = require './character'
items = require './items'
algos = require './algos'
module.exports.Scoring = (model) ->
MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change)
MODIFIER = .02 # each new level, armor, weapon add 2% modifier (this mechanism will change)
# {taskId} task you want to score
# {direction} 'up' or 'down'
# {times} # times to call score on this task (1 unless cron, usually)
# {update} if we're running updates en-mass (eg, cron on server) pass in userObj
score = (model, taskId, direction, times, batch, cron) ->
user = model.at '_user'
###
Calculates Exp & GP modification based on weapon & lvl
{value} task.value for gain
{modifiers} may manually pass in stats as {weapon, exp}. This is used for testing
###
expModifier = (value, modifiers = {}) ->
weapon = modifiers.weapon || user.get('items.weapon')
lvl = modifiers.lvl || user.get('stats.lvl')
dmg = items.items.weapon[weapon].modifier # each new weapon increases exp gain
dmg += (lvl-1) * MODIFIER # same for lvls
modified = value + (value * dmg)
return modified
commit = false
unless batch?
commit = true
batch = new character.BatchUpdate(model)
batch.startTransaction()
obj = batch.obj()
###
Calculates HP-loss modification based on armor & lvl
{value} task.value which is hurting us
{modifiers} may manually pass in modifier as {armor, lvl}. This is used for testing
###
hpModifier = (value, modifiers = {}) ->
armor = modifiers.armor || user.get('items.armor')
head = modifiers.head || user.get('items.head')
shield = modifiers.shield || user.get('items.shield')
lvl = modifiers.lvl || user.get('stats.lvl')
ac = items.items.armor[armor].modifier + items.items.head[head].modifier + items.items.shield[shield].modifier # each new armor decreases HP loss
ac += (lvl-1) * MODIFIER # same for lvls
modified = value - (value * ac)
return modified
{gp, hp, exp, lvl} = obj.stats
###
Calculates the next task.value based on direction
For negative values, use a line: something like y=-.1x+1
For positibe values, taper off with inverse log: y=.9^x
Would love to use inverse log for the whole thing, but after 13 fails it hits infinity. Revisit this formula later
{currentValue} the current value of the task, determines it's next value
{direction} 'up' or 'down'
###
taskDeltaFormula = (currentValue, direction) ->
sign = if (direction == "up") then 1 else -1
delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign)
return delta
taskPath = "tasks.#{taskId}"
taskObj = obj.tasks[taskId]
{type, value} = taskObj
priority = taskObj.priority or '!'
###
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
###
updateStats = (newStats, batch) ->
obj = batch.obj()
# If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it
if taskObj.value > obj.stats.gp and taskObj.type is 'reward'
r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)."
unless r
batch.commit()
return
# if user is dead, dont do anything
return if obj.stats.lvl == 0
delta = 0
times ?= 1
calculateDelta = (adjustvalue=true) ->
# If multiple days have passed, multiply times days missed
_.times times, (n) ->
# Each iteration calculate the delta (nextDelta), which is then accumulated in delta
# (aka, the total delta). This weirdness won't be necessary when calculating mathematically
# rather than iteratively
nextDelta = algos.taskDeltaFormula(value, direction)
value += nextDelta if adjustvalue
delta += nextDelta
if newStats.hp?
# Game Over
if newStats.hp <= 0
obj.stats.lvl = 0 # signifies dead
obj.stats.hp = 0
return
addPoints = ->
level = user.get('stats.lvl')
weaponStrength = items.items.weapon[user.get('items.weapon')].strength
exp += algos.expModifier(delta,weaponStrength,level, priority) / 2 # / 2 hack for now bcause people leveling too fast
gp += algos.gpModifier(delta, 1, priority)
subtractPoints = ->
level = user.get('stats.lvl')
armorDefense = items.items.armor[user.get('items.armor')].defense
helmDefense = items.items.head[user.get('items.head')].defense
shieldDefense = items.items.shield[user.get('items.shield')].defense
hp += algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level, priority)
switch type
when 'habit'
calculateDelta()
# Add habit value to habit-history (if different)
if (delta > 0) then addPoints() else subtractPoints()
taskObj.history ?= []
if taskObj.value != value
historyEntry = { date: +new Date, value: value }
taskObj.history.push historyEntry
batch.set "#{taskPath}.history", taskObj.history
when 'daily'
if cron? # cron
calculateDelta()
subtractPoints()
else
obj.stats.hp = newStats.hp
calculateDelta(false)
if delta != 0
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
if newStats.exp?
when 'todo'
if cron? #cron
calculateDelta()
#don't touch stats on cron
else
calculateDelta()
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'reward'
# Don't adjust values for rewards
calculateDelta(false)
# purchase item
gp -= Math.abs(taskObj.value)
num = parseFloat(taskObj.value).toFixed(2)
# if too expensive, reduce health & zero gp
if gp < 0
hp += gp # hp - gp difference
gp = 0
taskObj.value = value
batch.set "#{taskPath}.value", taskObj.value
origStats = _.clone obj.stats
updateStats model, {hp: hp, exp: exp, gp: gp}, batch
if commit
# newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
newStats = _.clone batch.obj().stats
_.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key]
batch.setStats(newStats)
# batch.setStats()
batch.commit()
return delta
###
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
###
updateStats = (model, newStats, batch) ->
user = model.at '_user'
obj = batch.obj()
# if user is dead, dont do anything
return if obj.stats.lvl == 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
obj.stats.lvl = 0 # signifies dead
obj.stats.hp = 0
return
else
obj.stats.hp = newStats.hp
if newStats.exp?
tnl = algos.tnl(obj.stats.lvl)
#silent = false
# if we're at level 100, turn xp to gold
if obj.stats.lvl >= 100
newStats.gp += newStats.exp / 15
newStats.exp = 0
obj.stats.lvl = 100
else
# level up & carry-over exp
tnl = model.get '_tnl'
if newStats.exp >= tnl
newStats.exp -= tnl
obj.stats.lvl++
#silent = true # push through the negative xp silently
user.set('stats.exp', newStats.exp) # push normal + notification
while newStats.exp >= tnl and obj.stats.lvl < 100 # keep levelling up
newStats.exp -= tnl
obj.stats.lvl++
tnl = algos.tnl(obj.stats.lvl)
if obj.stats.lvl== 100
newStats.exp = 0
obj.stats.hp = 50
obj.stats.exp = newStats.exp
obj.stats.exp = newStats.exp
#if silent
#console.log("pushing silent :" + obj.stats.exp)
#user.pass(true).set('stats.exp', obj.stats.exp)
# Set flags when they unlock features
if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1)
batch.set 'flags.customizationsNotification', true
obj.flags.customizationsNotification = true
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
# Set to object, then also send to browser right away to get model.on() subscription notification
batch.set 'flags.itemsEnabled', true
obj.flags.itemsEnabled = true
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
batch.set 'flags.partyEnabled', true
obj.flags.partyEnabled = true
if !obj.flags.petsEnabled and obj.stats.lvl >= 4
batch.set 'flags.petsEnabled', true
obj.flags.petsEnabled = true
# Set flags when they unlock features
if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1)
batch.set 'flags.customizationsNotification', true
obj.flags.customizationsNotification = true
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
# Set to object, then also send to browser right away to get model.on() subscription notification
batch.set 'flags.itemsEnabled', true
obj.flags.itemsEnabled = true
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
batch.set 'flags.partyEnabled', true
obj.flags.partyEnabled = true
if !obj.flags.petsEnabled and obj.stats.lvl >= 4
batch.set 'flags.petsEnabled', true
obj.flags.petsEnabled = true
if newStats.gp?
#FIXME what was I doing here? I can't remember, gp isn't defined
gp = 0.0 if (!gp? or gp<0)
obj.stats.gp = newStats.gp
if newStats.gp?
#FIXME what was I doing here? I can't remember, gp isn't defined
gp = 0.0 if (!gp? or gp<0)
obj.stats.gp = newStats.gp
# {taskId} task you want to score
# {direction} 'up' or 'down'
# {times} # times to call score on this task (1 unless cron, usually)
# {update} if we're running updates en-mass (eg, cron on server) pass in userObj
score = (taskId, direction, times, batch, cron) ->
commit = false
unless batch?
commit = true
batch = new character.BatchUpdate(model)
batch.startTransaction()
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
###
cron = (model) ->
user = model.at '_user'
today = +new Date
daysPassed = helpers.daysBetween(user.get('lastCron'), today, user.get('preferences.dayStart'))
if daysPassed > 0
batch = new character.BatchUpdate(model)
batch.startTransaction()
batch.set 'lastCron', today
obj = batch.obj()
hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss
# Tally each task
todoTally = 0
_.each obj.tasks, (taskObj) ->
{id, type, completed, repeat} = taskObj
if type in ['todo', 'daily']
# Deduct experience for missed Daily tasks,
# but not for Todos (just increase todo's value)
unless completed
# for todos & typical dailies, these are equivalent
daysFailed = daysPassed
# however, for dailys which have repeat dates, need
# to calculate how many they've missed according to their own schedule
if type=='daily' && repeat
daysFailed = 0
_.times daysPassed, (n) ->
thatDay = moment().subtract('days', n+1)
if repeat[helpers.dayMapping[thatDay.day()]]==true
daysFailed++
score model, id, 'down', daysFailed, batch, true
if type == 'daily'
if completed #set OHV for completed dailies
newValue = taskObj.value + algos.taskDeltaFormula(taskObj.value,'up')
batch.set "tasks.#{taskObj.id}.value", newValue
{gp, hp, exp, lvl} = obj.stats
taskPath = "tasks.#{taskId}"
taskObj = obj.tasks[taskId]
{type, value} = taskObj
# If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it
if taskObj.value > obj.stats.gp and taskObj.type is 'reward'
r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)."
unless r
batch.commit()
return
delta = 0
times ?= 1
calculateDelta = (adjustvalue=true) ->
# If multiple days have passed, multiply times days missed
_.times times, (n) ->
# Each iteration calculate the delta (nextDelta), which is then accumulated in delta
# (aka, the total delta). This weirdness won't be necessary when calculating mathematically
# rather than iteratively
nextDelta = taskDeltaFormula(value, direction)
value += nextDelta if adjustvalue
delta += nextDelta
addPoints = ->
modified = expModifier(delta)
exp += modified
gp += modified
subtractPoints = ->
modified = hpModifier(delta)
hp += modified
switch type
when 'habit'
# Don't adjust values for habits that don't have both + and -
adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true
calculateDelta(adjustvalue)
# Add habit value to habit-history (if different)
if (delta > 0) then addPoints() else subtractPoints()
taskObj.history ?= []
if taskObj.value != value
historyEntry = { date: +new Date, value: value }
taskObj.history.push historyEntry
batch.set "#{taskPath}.history", taskObj.history
when 'daily'
calculateDelta()
if cron? # cron
subtractPoints()
taskObj.history ?= []
taskObj.history.push { date: +new Date, value: taskObj.value }
batch.set "tasks.#{taskObj.id}.history", taskObj.history
batch.set "tasks.#{taskObj.id}.completed", false
else
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'todo'
calculateDelta()
unless cron? # don't touch stats on cron
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'reward'
# Don't adjust values for rewards
calculateDelta(false)
# purchase item
gp -= Math.abs(taskObj.value)
num = parseFloat(taskObj.value).toFixed(2)
# if too expensive, reduce health & zero gp
if gp < 0
hp += gp # hp - gp difference
gp = 0
taskObj.value = value
batch.set "#{taskPath}.value", taskObj.value
origStats = _.clone obj.stats
updateStats {hp: hp, exp: exp, gp: gp}, batch
if commit
# newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
newStats = _.clone batch.obj().stats
_.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key]
batch.setStats(newStats)
# batch.setStats()
batch.commit()
return delta
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
###
cron = () ->
today = +new Date
daysPassed = helpers.daysBetween(today, user.get('lastCron'))
if daysPassed > 0
batch = new character.BatchUpdate(model)
batch.startTransaction()
batch.set 'lastCron', today
obj = batch.obj()
hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss
# Tally each task
todoTally = 0
_.each obj.tasks, (taskObj) ->
{id, type, completed, repeat} = taskObj
if type in ['todo', 'daily']
# Deduct experience for missed Daily tasks,
# but not for Todos (just increase todo's value)
unless completed
# for todos & typical dailies, these are equivalent
daysFailed = daysPassed
# however, for dailys which have repeat dates, need
# to calculate how many they've missed according to their own schedule
if type=='daily' && repeat
daysFailed = 0
_.times daysPassed, (n) ->
thatDay = moment().subtract('days', n+1)
if repeat[helpers.dayMapping[thatDay.day()]]==true
daysFailed++
score id, 'down', daysFailed, batch, true
if type == 'daily'
taskObj.history ?= []
taskObj.history.push { date: +new Date, value: value }
batch.set "tasks.#{taskObj.id}.history", taskObj.history
batch.set "tasks.#{taskObj.id}.completed", false
value = obj.tasks[taskObj.id].value #get updated value
absVal = if (completed) then Math.abs(value) else value
todoTally += absVal
else if type is 'habit' # slowly reset 'onlies' value to 0
if taskObj.up==false or taskObj.down==false
if Math.abs(taskObj.value) < 0.1
batch.set "tasks.#{taskObj.id}.value", 0
else
value = obj.tasks[taskObj.id].value #get updated value
absVal = if (completed) then Math.abs(value) else value
todoTally += absVal
batch.set "tasks.#{taskObj.id}.value", taskObj.value / 2
# Finished tallying
obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= []
obj.history.todos.push { date: today, value: todoTally }
# tally experience
expTally = obj.stats.exp
lvl = 0 #iterator
while lvl < (obj.stats.lvl-1)
lvl++
expTally += (lvl*100)/5
obj.history.exp.push { date: today, value: expTally }
# Finished tallying
obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= []
obj.history.todos.push { date: today, value: todoTally }
# tally experience
expTally = obj.stats.exp
lvl = 0 #iterator
while lvl < (obj.stats.lvl-1)
lvl++
expTally += algos.tnl(lvl)
obj.history.exp.push { date: today, value: expTally }
# Set the new user specs, and animate HP loss
[hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore]
batch.setStats()
batch.set('history', obj.history)
batch.commit()
browser.resetDom(model)
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
# Set the new user specs, and animate HP loss
[hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore]
batch.setStats()
batch.set('history', obj.history)
batch.commit()
browser.resetDom(model)
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
return {
MODIFIER: MODIFIER
score: score
cron: cron
module.exports = {
score: score
cron: cron
# testing stuff
expModifier: expModifier
hpModifier: hpModifier
taskDeltaFormula: taskDeltaFormula
}
# testing stuff
expModifier: algos.expModifier
hpModifier: algos.hpModifier
taskDeltaFormula: algos.taskDeltaFormula
}

View file

@ -2,48 +2,45 @@ scoring = require './scoring'
helpers = require './helpers'
_ = require 'underscore'
moment = require 'moment'
character = require './character'
module.exports.view = (view) ->
view.fn 'taskClasses', (type, completed, value, repeat, tags = {}, filters = {}) ->
#TODO figure out how to just pass in the task model, so i can access all these properties from one object
if type != 'reward'
for filter, enabled of filters
if enabled and not tags[filter]
# All the other classes don't matter
return 'filtered-out'
view.fn 'taskClasses', (task, filters) ->
return unless task
{type, completed, value, repeat} = task
for filter, enabled of filters
if enabled and not task.tags?[filter]
# All the other classes don't matter
return 'hide'
classes = type
# show as completed if completed (naturally) or not required for today
if completed or (repeat and repeat[helpers.dayMapping[moment().day()]]==false)
classes += " completed"
else
classes += " uncompleted"
if type in ['todo', 'daily']
if completed or (repeat and repeat[helpers.dayMapping[moment().day()]]==false)
classes += " completed"
else
classes += " uncompleted"
switch
when value<-8 then classes += ' color-worst'
when value>=-8 and value<-5 then classes += ' color-worse'
when value>=-5 and value<-1 then classes += ' color-bad'
when value>=-1 and value<1 then classes += ' color-neutral'
when value>=1 and value<5 then classes += ' color-good'
when value>=5 and value<10 then classes += ' color-better'
when value>=10 then classes += ' color-best'
if value < -20
classes += ' color-worst'
else if value < -10
classes += ' color-worse'
else if value < -1
classes += ' color-bad'
else if value < 1
classes += ' color-neutral'
else if value < 5
classes += ' color-good'
else if value < 10
classes += ' color-better'
else
classes += ' color-best'
return classes
module.exports.app = (appExports, model) ->
user = model.at('_user')
score = new scoring.Scoring(model)
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
return if passed? && passed.cron # Don't do this stuff on cron
direction = () ->
return 'up' if completed==true and previous == false
return 'down' if completed==false and previous == true
throw new Error("Direction neither 'up' nor 'down' on checkbox set.")
# Score the user based on todo task
task = user.at("tasks.#{i}")
score.score(i, direction())
appExports.addTask = (e, el, next) ->
type = $(el).attr('data-task-type')
@ -76,7 +73,7 @@ module.exports.app = (appExports, model) ->
appExports.del = (e, el) ->
# Derby extends model.at to support creation from DOM nodes
task = model.at(e.target)
task = e.at()
id = task.get('id')
history = task.get('history')
@ -88,7 +85,7 @@ module.exports.app = (appExports, model) ->
return # Cancel. Don't delete, don't hurt user
else
task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox"
score.score(id, direction:'down')
scoring.score(model, id, direction:'down')
# prevent accidently deleting long-standing tasks
else
@ -124,8 +121,8 @@ module.exports.app = (appExports, model) ->
appExports.toggleTaskEdit = (e, el) ->
hideId = $(el).attr('data-hide-id')
toggleId = $(el).attr('data-toggle-id')
$(document.getElementById(hideId)).hide()
$(document.getElementById(toggleId)).toggle()
$(document.getElementById(hideId)).addClass('visuallyhidden')
$(document.getElementById(toggleId)).toggleClass('visuallyhidden')
appExports.toggleChart = (e, el) ->
hideSelector = $(el).attr('data-hide-id')
@ -142,9 +139,9 @@ module.exports.app = (appExports, model) ->
data = google.visualization.arrayToDataTable matrix
options = {
title: 'History'
#TODO use current background color: $(el).css('background-color), but convert to hex (see http://goo.gl/ql5pR)
backgroundColor: 'whiteSmoke'
title: 'History'
#TODO use current background color: $(el).css('background-color), but convert to hex (see http://goo.gl/ql5pR)
backgroundColor: 'whiteSmoke'
}
chart = new google.visualization.LineChart(document.getElementById( chartSelector ))
@ -180,9 +177,70 @@ module.exports.app = (appExports, model) ->
path = "_user.tasks.#{taskId}.tags.#{tagId}"
model.set path, !(model.get path)
appExports.score = (e, el, next) ->
setUndo = (stats, task) ->
previousUndo = model.get('_undo')
clearTimeout(previousUndo.timeoutId) if previousUndo?.timeoutId
timeoutId = setTimeout (-> model.del('_undo')), 10000
model.set '_undo', {stats:stats, task:task, timeoutId: timeoutId}
###
Call scoring functions for habits & rewards (todos & dailies handled below)
###
appExports.score = (e, el) ->
task= model.at $(el).parents('li')[0]
taskObj = task.get()
direction = $(el).attr('data-direction')
direction = 'up' if direction == 'true/'
direction = 'down' if direction == 'false/'
task = model.at $(el).parents('li')[0]
score.score(task.get('id'), direction)
# set previous state for undo
setUndo _.clone(user.get('stats')), _.clone(taskObj)
scoring.score(model, taskObj.id, direction)
###
This is how we handle appExports.score for todos & dailies. Due to Derby's special handling of `checked={:task.completd}`,
the above function doesn't work so we need a listener here
###
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
return if passed? && passed.cron # Don't do this stuff on cron
direction = if completed then 'up' else 'down'
# set previous state for undo
taskObj = _.clone user.get("tasks.#{i}")
taskObj.completed = previous
setUndo _.clone(user.get('stats')), taskObj
scoring.score(model, i, direction)
###
Undo
###
appExports.undo = () ->
undo = model.get '_undo'
clearTimeout(undo.timeoutId) if undo?.timeoutId
batch = character.BatchUpdate(model)
batch.startTransaction()
model.del '_undo'
_.each undo.stats, (val, key) -> batch.set "stats.#{key}", val
taskPath = "tasks.#{undo.task.id}"
_.each undo.task, (val, key) ->
return if key in ['id', 'type'] # strange bugs in this world: https://workflowy.com/shared/a53582ea-43d6-bcce-c719-e134f9bf71fd/
if key is 'completed'
user.pass({cron:true}).set("#{taskPath}.completed",val)
else
batch.set "#{taskPath}.#{key}", val
batch.commit()
appExports.tasksToggleAdvanced = (e, el) ->
$(el).next('.advanced-option').toggleClass('visuallyhidden')
appExports.tasksSaveAndClose = ->
# When they update their notes, re-establish tooltip & popover
$('[rel=tooltip]').tooltip()
$('[rel=popover]').popover()
appExports.tasksSetPriority = (e, el) ->
dataId = $(el).parent('[data-id]').attr('data-id')
#"_user.tasks.#{dataId}"
model.at(e.target).set 'priority', $(el).attr('data-priority')

View file

@ -3,82 +3,191 @@ router = new express.Router()
scoring = require '../app/scoring'
_ = require 'underscore'
icalendar = require('icalendar')
{ tnl } = require '../app/algos'
validator = require 'derby-auth/node_modules/validator'
check = validator.check
sanitize = validator.sanitize
# ---------- /v1 API ------------
# Every url added beneath router is prefaced by /v1
NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request"
NO_USER_FOUND = err: "No user found."
# ---------- /api/v1 API ------------
# Every url added beneath router is prefaced by /api/v1
###
v1 API. Requires user-id and apiToken, task-id, direction. Test with:
curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up
v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with:
$ cd node_modules/racer && npm install && cd ../..
$ mocha test/api.mocha.coffee
###
router.post '/users/:uid/tasks/:taskId/:direction', (req, res) ->
{uid, taskId, direction} = req.params
{apiToken, title, service, icon} = req.body
console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development'
###
API Status
###
router.get '/status', (req, res) ->
res.json status: 'up'
###
beforeEach auth interceptor
###
auth = (req, res, next) ->
uid = req.headers['x-api-user']
token = req.headers['x-api-key']
return res.json 401, NO_TOKEN_OR_UID unless uid || token
model = req.getModel()
query = model.query('users').withIdAndToken(uid, token)
query.fetch (err, user) ->
return res.json err: err if err
req.user = user
req.userObj = user.get()
return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj)
req._isServer = true
next()
###
GET /user
###
router.get '/user', auth, (req, res) ->
user = req.userObj
user.stats.toNextLevel = tnl user.stats.lvl
user.stats.maxHealth = 50
delete user.apiToken
res.json user
###
GET /user/task/:id
###
router.get '/user/task/:id', auth, (req, res) ->
task = req.userObj.tasks[req.params.id]
return res.json 400, err: "No task found." if !task || _.isEmpty(task)
res.json 200, task
###
validate task
###
validateTask = (req, res, next) ->
task = {}
newTask = { type, text, notes, value, up, down, completed } = req.body
# If we're updating, get the task from the user
if req.method is 'PUT' or req.method is 'DELETE'
task = req.userObj?.tasks[req.params.id]
return res.json 400, err: "No task found." if !task || _.isEmpty(task)
# Strip for now
type = undefined
delete newTask.type
else if req.method is 'POST'
unless /^(habit|todo|daily|reward)$/.test type
return res.json 400, err: 'type must be habit, todo, daily, or reward'
text = sanitize(text).xss()
notes = sanitize(notes).xss()
value = sanitize(value).toInt()
switch type
when 'habit'
newTask.up = true unless typeof up is 'boolean'
newTask.down = true unless typeof down is 'boolean'
when 'daily', 'todo'
newTask.completed = false unless typeof completed is 'boolean'
_.extend task, newTask
req.task = task
next()
###
PUT /user/task/:id
###
router.put '/user/task/:id', auth, validateTask, (req, res) ->
req.user.set "tasks.#{req.task.id}", req.task
res.json 200, req.task
###
DELETE /user/task/:id
###
router.delete '/user/task/:id', auth, validateTask, (req, res) ->
taskIds = req.user.get "#{req.task.type}Ids"
req.user.del "tasks.#{req.task.id}"
# Remove one id from array of typeIds
req.user.remove "#{req.task.type}Ids", taskIds.indexOf(req.task.id), 1
res.send 204
###
POST /user/task/
###
router.post '/user/task', auth, validateTask, (req, res) ->
task = req.task
type = task.type
model = req.getModel()
model.ref '_user', req.user
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
model.at("_#{type}List").push task
res.json 201, task
###
GET /user/tasks
###
router.get '/user/tasks', auth, (req, res) ->
user = req.userObj
return res.json 400, NO_USER_FOUND if !user || _.isEmpty(user)
model = req.getModel()
model.ref '_user', req.user
tasks = []
types = ['habit','todo','daily','reward']
if /^(habit|todo|daily|reward)$/.test req.query.type
types = [req.query.type]
for type in types
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
tasks = tasks.concat model.get("_#{type}List")
res.json 200, tasks
###
This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
###
scoreTask = (req, res, next) ->
{taskId, direction} = req.params
{title, service, icon} = req.body
# Send error responses for improper API call
return res.send(500, 'request body "apiToken" required') unless apiToken
return res.send(500, ':uid required') unless uid
return res.send(500, ':taskId required') unless taskId
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
req._isServer = true
model = req.getModel()
model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) ->
return res.send(500, err) if err
user = result
userObj = user.get()
if _.isEmpty(userObj)
return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id")
{user, userObj} = req
model.ref('_user', user)
model.ref('_user', user)
# Create task if doesn't exist
# TODO add service & icon to task
unless model.get("_user.tasks.#{taskId}")
model.refList "_habitList", "_user.tasks", "_user.habitIds"
model.at('_habitList').push
id: taskId
type: 'habit'
text: (title || taskId)
value: 0
up: true
down: true
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
# Create task if doesn't exist
# TODO add service & icon to task
unless model.get("_user.tasks.#{taskId}")
model.refList "_habitList", "_user.tasks", "_user.habitIds"
model.at('_habitList').push
id: taskId
type: 'habit'
text: (title || taskId)
value: 0
up: true
down: true
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
score = scoring.Scoring(model)
delta = score.score(taskId, direction)
result = model.get ('_user.stats')
result.delta = delta
res.send(result)
delta = scoring.score(model, taskId, direction)
result = model.get ('_user.stats')
result.delta = delta
res.send(result)
router.get '/users/:uid/calendar.ics', (req, res) ->
#return next() #disable for now
{uid} = req.params
{apiToken} = req.query
model = req.getModel()
query = model.query('users').withIdAndToken(uid, apiToken)
query.fetch (err, result) ->
return res.send(500, err) if err
tasks = result.get('tasks')
# tasks = result[0].tasks
tasksWithDates = _.filter tasks, (task) -> !!task.date
return res.send(500, "No events found") if _.isEmpty(tasksWithDates)
ical = new icalendar.iCalendar()
ical.addProperty('NAME', 'HabitRPG')
_.each tasksWithDates, (task) ->
event = new icalendar.VEvent(task.id);
event.setSummary(task.text);
d = new Date(task.date)
d.date_only = true
event.setDate d
ical.addComponent event
res.type('text/calendar')
formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:')
res.send(200, formattedIcal)
router.post '/user/tasks/:taskId/:direction', auth, scoreTask
module.exports = router
module.exports.auth = auth
module.exports.scoreTask = scoreTask # export so deprecated can call it

View file

@ -1,6 +1,11 @@
express = require 'express'
router = new express.Router()
scoring = require '../app/scoring'
_ = require 'underscore'
icalendar = require('icalendar')
api = require './api'
# ---------- Deprecated Paths ------------
deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'
@ -9,4 +14,39 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage)
router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
# Redirect to new API
initDeprecated = (req, res, next) ->
req.headers['x-api-user'] = req.params.uid
req.headers['x-api-key'] = req.body.apiToken
next()
router.post '/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, api.scoreTask
router.get '/v1/users/:uid/calendar.ics', (req, res) ->
#return next() #disable for now
{uid} = req.params
{apiToken} = req.query
model = req.getModel()
query = model.query('users').withIdAndToken(uid, apiToken)
query.fetch (err, result) ->
return res.send(500, err) if err
tasks = result.get('tasks')
# tasks = result[0].tasks
tasksWithDates = _.filter tasks, (task) -> !!task.date
return res.send(500, "No events found") if _.isEmpty(tasksWithDates)
ical = new icalendar.iCalendar()
ical.addProperty('NAME', 'HabitRPG')
_.each tasksWithDates, (task) ->
event = new icalendar.VEvent(task.id);
event.setSummary(task.text);
d = new Date(task.date)
d.date_only = true
event.setDate d
ical.addComponent event
res.type('text/calendar')
formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:')
res.send(200, formattedIcal)
module.exports = router

View file

@ -31,7 +31,7 @@ server = http.createServer expressApp
module.exports = server
derby.use require('racer-db-mongo')
store = derby.createStore
module.exports.habitStore = store = derby.createStore
db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true}
listen: server
@ -57,6 +57,7 @@ auth.store(store, habitrpgStore.customAccessControl)
mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
expressApp
.use(middleware.allowCrossDomain)
.use(express.favicon("#{publicPath}/favicon.ico"))
# Gzip static files and serve from memory
.use(gzippo.staticGzip(publicPath, maxAge: ONE_YEAR))
@ -74,6 +75,9 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
)
# Adds req.getModel method
.use(store.modelMiddleware())
# API should be hit before all other routes
.use('/api/v1', require('./api').middleware)
.use(require('./deprecated').middleware)
# Show splash page for newcomers
.use(middleware.splash)
.use(priv.middleware)
@ -81,9 +85,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
.use(auth.middleware(strategies, options))
# Creates an express middleware from the app's routes
.use(app.router())
.use('/v1', require('./api').middleware)
.use(require('./static').middleware)
.use(require('./deprecated').middleware)
.use(expressApp.router)
.use(serverError(root))

View file

@ -1,17 +1,27 @@
module.exports.splash = (req, res, next) ->
# This was an API call, not a page load
return next() if req.is('json')
unless req.query?.play? or req.getModel().get('_userId')
res.redirect('/splash.html')
isStatic = req.url.split('/')[1] is 'static'
unless req.query?.play? or req.getModel().get('_userId') or isStatic
res.redirect('/static/front')
else
next()
module.exports.view = (req, res, next) ->
model = req.getModel()
_view = model.get('_view') || {}
## Set _mobileDevice to true or false so view can exclude portions from mobile device
_view.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent')
_view.nodeEnv = process.env.NODE_ENV
model.set '_view', _view
model = req.getModel()
_view = model.get('_view') || {}
## Set _mobileDevice to true or false so view can exclude portions from mobile device
_view.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent')
_view.nodeEnv = process.env.NODE_ENV
model.set '_view', _view
next()
#CORS middleware
module.exports.allowCrossDomain = (req, res, next) ->
res.header "Access-Control-Allow-Origin", (req.headers.origin || "*")
res.header "Access-Control-Allow-Methods", "OPTIONS,GET,POST,PUT,HEAD,DELETE"
res.header "Access-Control-Allow-Headers", "Content-Type,X-Requested-With,x-api-user,x-api-key"
# wtf is this for?
if req.method is 'OPTIONS'
res.send(200);
else
next()

View file

@ -7,10 +7,18 @@ derby = require 'derby'
# ---------- Static Pages ------------
staticPages = derby.createStatic path.dirname(path.dirname(__dirname))
router.get '/privacy', (req, res) ->
staticPages.render 'privacy', res
beforeEach = (req, res, next) ->
req.getModel().set '_view', {nodeEnv: 'production'} # we don't want cheat buttons on static pages
next()
router.get '/terms', (req, res) ->
staticPages.render 'terms', res
router.get '/splash.html', (req, res) -> return res.redirect('/static/front')
router.get '/static/front', beforeEach, (req, res) -> staticPages.render 'static/front', res
router.get '/static/about', beforeEach, (req, res) -> staticPages.render 'static/about', res
router.get '/static/team', beforeEach, (req, res) -> staticPages.render 'static/team', res
router.get '/static/extensions', beforeEach, (req, res) -> staticPages.render 'static/extensions', res
router.get '/static/faq', beforeEach, (req, res) -> staticPages.render 'static/faq', res
router.get '/static/privacy', beforeEach, (req, res) -> staticPages.render 'static/privacy', res
router.get '/static/terms', beforeEach, (req, res) -> staticPages.render 'static/terms', res
module.exports = router

View file

@ -28,7 +28,10 @@ userAccess = (store) ->
store.writeAccess "*", "users.*", -> # captures, value, accept, err ->
accept = arguments[arguments.length-2]
err = arguments[arguments.length - 1]
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
return accept(true) if derbyAuth.isServer(@)
return accept(false) if derbyAuth.bustedSession(@)
captures = arguments[0].split('.')
@ -40,8 +43,7 @@ userAccess = (store) ->
return accept(true)
# Same session (user.id = this.session.userId)
if (uid is @session.userId) or derbyAuth.isServer(@)
return accept(true)
return accept(true) if uid is @session.userId
accept(false)
@ -51,9 +53,10 @@ userAccess = (store) ->
oldBalance = @session.req?._racerModel?.get("users.#{id}.balance") || 0
purchasingSomethingOnClient = newBalance < oldBalance
accept(purchasingSomethingOnClient or @session.req?._isServer)
accept(purchasingSomethingOnClient or derbyAuth.isServer(@))
store.writeAccess "*", "users.*.flags.ads", -> # captures, value, accept, err ->
accept = arguments[arguments.length - 2]
err = arguments[arguments.length - 1]
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
return accept(false) if derbyAuth.bustedSession(@)
@ -67,7 +70,7 @@ userAccess = (store) ->
###
REST = (store) ->
store.query.expose "users", "withIdAndToken", (uid, token) ->
@byId(uid)
@where("id").equals(uid)
.where('apiToken').equals(token)
.findOne()

View file

@ -8,6 +8,8 @@ lvlColor = #d9edf7
lvlText = #3a87ad
xpColor = #DCC0FB
xpText = #635673
deathColor = #4E4E4E
deathText = #ABABAB
borderDarken = 20%
// alert styles
@ -32,11 +34,36 @@ borderDarken = 20%
border-color: darken(xpColor,borderDarken)
color: xpText
.alert-death
background-color: deathColor
border-color: deathColor
color: deathText
// alert icons
.icon-gp
background: url("img/sprites/shop_sprites.png") no-repeat
background-size: 400px
background-position: -386px 0
.icon-gold
background: url("img/coin_single_gold.png") no-repeat
background-position: center center
background-size: 18px
width: 14px
height: 14px
.icon-silver
background: url("img/coin_single_silver.png") no-repeat
background-position: center center
background-size: 18px
width: 14px
height: 14px
.icon-death
background: url("img/sprites/dead.png") no-repeat
background-position: center center
background-size: 14px
width: 14px
height: 14px
.undo-button
position:absolute
left:5px
top:5px

32
styles/app/helpers.styl Normal file
View file

@ -0,0 +1,32 @@
// Hide from both screenreaders and browsers: h5bp.com/u
.hidden
display: none !important
visibility: hidden
// Hide only visually, but have it available for screenreaders: h5bp.com/v
.visuallyhidden
border: 0
clip: rect(0 0 0 0)
height: 1px
margin: -1px
overflow: hidden
padding: 0
position: absolute
width: 1px
// Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p
.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus
clip: auto
height: auto
margin: 0
overflow: visible
position: static
width: auto
// Hide visually and from screenreaders, but maintain layout
.invisible
visibility: hidden

View file

@ -8,13 +8,31 @@
@import "./items.styl";
@import "./alerts.styl";
@import "./filters.styl";
@import "./helpers.styl";
@import "./responsive.styl";
@import "./scrollbars.styl";
@import "../../public/vendor/bootstrap-datepicker/css/datepicker.css";
// fix exploding to very wide for some reason
.datepicker
width: 210px
html,body,p,h1,ul,li,table,tr,th,td
margin: 0
padding: 0
*
-webkit-box-sizing: border-box
-moz-box-sizing: border-box
box-sizing: border-box
hr
border-top: 0
border-bottom: 1px solid #ddd
border-color: rgba(0,0,0,0.1)
/* Header
-------------------------------------------------- */
@ -35,7 +53,7 @@ html,body,p,h1,ul,li,table,tr,th,td
box-sizing: border-box
z-index: 1000
@media (max-width: 480px) {
@media (max-width: 600px) {
#head {
position: static;
}
@ -97,16 +115,6 @@ html,body,p,h1,ul,li,table,tr,th,td
/* Footer
-------------------------------------------------- */
.footer ul li
list-style:none
float:left
.footer ul li:after
content: "\00A0\00A0|\00A0\00A0"
.footer ul li:last-child:after
content: ""
.footer table td
vertical-align: top
/* Bootstrap website boilerplate */
.footer {
padding: 70px 0;
@ -132,7 +140,7 @@ html,body,p,h1,ul,li,table,tr,th,td
/*overflow:auto;*/
padding-bottom: 250px; /* don't know why this works, sticky footers are weird */
@media (max-width: 480px) {
@media (max-width: 600px) {
#wrap {
margin-top: 0
}

View file

@ -1,42 +1,83 @@
/* ----- Items, Weapons, Armor -----*/
.item-store-popover img
float:left
padding-right:7px;
// money styles
.money
display: inline-block
line-height: 1.5em
padding-left: 0.75em
.item .task-text img
max-height: 16px
[class^="shop_"]
vertical-align: top
display: inline-block
.buy-link, .item-buy-link
background-color: #DEE5F2
padding: 2px
margin-right: 10px
border-radius: 5px
.btn-buy
width: 4.5em
height: 3em
padding: 0.75em 0 0
text-align: center
vertical-align: top
background-color: $better
img
height:20px
color: #555
&:hover, &:focus
background-color: $bad
text-decoration: none
.item-buy-link
background-color: #ccffcc
.rewards
margin-bottom: 1.5em
padding-bottom: 1.5em
border-bottom: 1px solid rgba(0,0,0,0.1)
.item
position:relative
.reward-item
background: white
.shop-table
position:absolute
top: 0
left: 50px
.reward-cost
display: inline
#money
position:relative
.shop_gold, .shop_silver, .shop_copper
float:right
.money-table
position:absolute
top: 0
.shop-pet
mouse: pointer
.shop-pet-owned-check
position:absolute
right:0px
bottom:0px
// store items
.btn-reroll
background-color: $better
cursor: pointer
box-shadow: inset -1px -1px 0 rgba(0,0,0,0.1)
&:hover, &:focus
background-color: $bad
.item-img
display: inline-block
vertical-align: top
// background-color: $bad
height: 3em
margin-left: -0.5em
margin-right: 0.5em
// border: 1px solid rgba(0,0,0,0.1)
border-top: 0
border-bottom: 0
.token-wallet
cursor: pointer
.tile
background-color: darken($neutral, 10%)
.add-token-btn
opacity: 0
&:hover, &:focus
.add-token-btn
opacity: 1
background-color: $good
.tile
opacity: 1
// pets (this will all change when pet system is overhauled)
.pet-grid
padding-top: 1.5em
border-top: rgba(0,0,0,0.1)
table
width: 100%
td
padding: 0.5em
width: 25%
&.active-pet
background-color: $bad
outline: 1px solid rgba(0,0,0,0.1)
outline-offset: -1px
&:hover, &:focus
background-color: darken($better, 10%)

View file

@ -0,0 +1,31 @@
.grid
padding: 0 1.5em 1.5em 0
letter-spacing: -4px
.module
display: inline-block
letter-spacing: normal
vertical-align: top
width: 25%
padding: 0 0 1.5em 1.5em
-moz-box-sizing: border-box
box-sizing: border-box
// at 960px when 4 columns really starts to break
@media (max-width: 60em)
.module
width: 50%
.task-column
max-height: 18em
overflow-y: scroll
// at 600px when 2col starts breaking
@media (max-width: 37.5em)
.grid
padding-right: 0
.module
width: 100%
padding-left: 0
.task-column
max-height: none
overflow: visible

View file

@ -0,0 +1,82 @@
/* Gmail style scrollbar */
.task-column::-webkit-scrollbar {
width: 12px
}
.task-column::-webkit-scrollbar-thumb {
border-width: 1px 1px 1px 2px
}
.task-column::-webkit-scrollbar-track {
border-width: 0
}
.task-column::-webkit-scrollbar {
height: 16px;
overflow: visible;
width: 16px;
}
.task-column::-webkit-scrollbar-button {
height: 0;
width: 0;
}
.task-column::-webkit-scrollbar-track {
background-clip: padding-box;
border: solid transparent;
border-width: 0 0 0 4px;
}
.task-column::-webkit-scrollbar-track:horizontal {
border-width: 4px 0 0
}
.task-column::-webkit-scrollbar-track:hover {
background-color: rgba(150,150,150,.05);
box-shadow: inset 1px 0 0 rgba(150,150,150,.1);
}
.task-column::-webkit-scrollbar-track:horizontal:hover {
box-shadow: inset 0 1px 0 rgba(150,150,150,.1)
}
.task-column::-webkit-scrollbar-track:active {
background-color: rgba(150,150,150,.05);
box-shadow: inset 1px 0 0 rgba(150,150,150,.14),inset -1px 0 0 rgba(150,150,150,.07);
}
.task-column::-webkit-scrollbar-track:horizontal:active {
box-shadow: inset 0 1px 0 rgba(150,150,150,.14),inset 0 -1px 0 rgba(150,150,150,.07)
}
.task-column::-webkit-scrollbar-thumb {
background-color: rgba(150,150,150,.2);
background-clip: padding-box;
border: solid transparent;
border-width: 1px 1px 1px 6px;
min-height: 28px;
padding: 100px 0 0;
box-shadow: inset 1px 1px 0 rgba(150,150,150,.1),inset 0 -1px 0 rgba(150,150,150,.07);
}
.task-column::-webkit-scrollbar-thumb:horizontal {
border-width: 6px 1px 1px;
padding: 0 0 0 100px;
box-shadow: inset 1px 1px 0 rgba(150,150,150,.1),inset -1px 0 0 rgba(150,150,150,.07);
}
.task-column::-webkit-scrollbar-thumb:hover {
background-color: rgba(150,150,150,.4);
box-shadow: inset 1px 1px 1px rgba(150,150,150,.25);
}
.task-column::-webkit-scrollbar-thumb:active {
background-color: rgba(150,150,150,0.5);
box-shadow: inset 1px 1px 3px rgba(150,150,150,0.35);
}
.task-column::-webkit-scrollbar-track {
border-width: 0 1px 0 6px
}
.task-column::-webkit-scrollbar-track:horizontal {
border-width: 6px 0 1px
}
.task-column::-webkit-scrollbar-track:hover {
background-color: rgba(150,150,150,.035);
box-shadow: inset 1px 1px 0 rgba(150,150,150,.14),inset -1px -1px 0 rgba(150,150,150,.07);
}
.task-column::-webkit-scrollbar-thumb {
border-width: 0 1px 0 6px
}
.task-column::-webkit-scrollbar-thumb:horizontal {
border-width: 6px 0 1px
}
.task-column::-webkit-scrollbar-corner {
background: transparent
}

View file

@ -49,6 +49,6 @@
.shop_armor_1 {background-position: -800px 0; width: 40px; height: 40px}
.shop_reroll {background-position: -840px 0; width: 40px; height: 40px}
.shop_potion {background-position: -880px 0; width: 40px; height: 40px}
.shop_copper {background-position: -920px 0; width: 40px; height: 40px}
.shop_silver {background-position: -960px 0; width: 40px; height: 40px}
.shop_gold {background-position: -1000px 0; width: 40px; height: 40px}
.shop_copper {background-position: -923px -8px; width: 32px; height: 22px}
.shop_silver {background-position: -963px -8px; width: 32px; height: 22px}
.shop_gold {background-position: -1003px -8px; width: 32px; height: 22px}

View file

@ -1,42 +1,354 @@
.color-worst pre
background-color: rgb(230, 184, 175)
.color-worse pre
background-color: rgb(244, 204, 204)
.color-bad pre
background-color: rgb(252, 229, 205)
.color-neutral pre
background-color: rgb(255, 242, 204)
.color-good pre
background-color: rgb(217, 234, 211)
.color-better pre
background-color: rgb(208, 224, 227)
.color-best pre
background-color: rgb(201, 218, 248)
.completed pre
background-color: rgb(217, 217, 217)
color: rgb(153, 153, 153)
.reward pre
// 1. Set up color classes
// ========================
// color variables
$worst = rgb(230, 184, 175)
$worse = rgb(244, 204, 204)
$bad = rgb(252, 229, 205)
$neutral = rgb(255, 242, 204)
$good = rgb(217, 234, 211)
$better = rgb(208, 224, 227)
$best = rgb(201, 218, 248)
$completed = rgb(217, 217, 217)
// array of keywords and their associated color vars
$stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $good) (better $better) (best $best)
// for each color stage, generate a named class w/ the appropriate color
for $stage in $stages
.color-{$stage[0]}
background-color: $stage[1]
.action-yesno label,
.task-action-btn
background-color: darken($stage[1], 30%)
&:hover, &:focus
background-color: darken($stage[1], 40%)
// completed has to be outside the loop to override the color class
.completed
background-color: $completed
color: #999
.task-checker label
background-color: darken($completed, 30%)
.task-action-btn
background-color: darken($completed, 30%)
.reward
background-color: white
label.checkbox.inline{
width: 40px
// 2. Columns & Tasks
// ===================
// main columns
// ------------
.task-column
padding: 1.5em
background: #f5f5f5
border: 1px solid #ccc
font-family: 'Lato', sans-serif
.task-column_title
margin: 0 0 0.5em
padding: 0
// add new task form
// -----------------
.addtask-form
margin-bottom: 0.75em
outline: 1px solid rgba(0,0,0,0.15)
outline-offset: -1px
background-color: white
// box-shadow: 0 0 3px rgba(0,0,0,0.15)
position: relative
// the input field
.addtask-field
display: block
width: 100%
input
font-family: 'Lato', sans-serif
border: 0
outline: 0
border-radius: 0
box-shadow: none
background-color: white
height: 3em
padding: 0 0 0 0.5em
width: 100%
&:focus
box-shadow: inset 0 0 3px darken($best, 20%),inset -1px 0 1px darken($best, 30%)
// the button
.addtask-btn
position: absolute
right: 0
top: 0
// important for overriding bootstrap, remove later
width: 1.81818em !important
height: 1.88em
padding: 0
font-size: 1.61em
line-height: 1.7
outline: 0
border: 0
border-radius: 0 //required to nuke BB-playbook user agent styles
background-color: darken($best, 20%)
opacity: 0.75
&:hover,
&:focus
opacity: 1
background-color: darken($best, 20%)
&:active
background-color: darken($best, 30%)
opacity: 1
// an individual task entry
// ------------------------
.task
overflow: hidden
list-style: none
clear: both
padding: 0 0.5em 0 0.5em
min-height: 3em
line-height: 3
margin-bottom: 0.75em
outline: 1px solid rgba(0,0,0,0.1)
outline-offset: -1px
&:hover
cursor: move
&:last-child
margin-bottom: 0
// task content
.task-text
display: inline
// when a task is being dragged
.task.ui-sortable-helper {
box-shadow: 0 0 3px rgba(0,0,0,0.15), 0 0 5px rgba(0,0,0,0.1)
transform: scale(1.05)
outline: 1px solid rgba(0,0,0,0.2)
}
.todos
.nav-pills > .active > a, .nav-pills > .active > a:hover
color: #005580
background-color: #DEE5F2
// primary task commands
// ----------------------
.task-controls
display: inline
margin-left: -0.5em
margin-right: 0.5em
.dailys
.repeat-days > .btn:not(.active)
background-color: #aaa;
background-image: -moz-linear-gradient(top, #eee, #aaa);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#eee), to(#aaa));
background-image: -webkit-linear-gradient(top, #eee, #aaa);
background-image: -o-linear-gradient(top, #eee, #aaa);
background-image: linear-gradient(to bottom, #eee, #aaa);
background-repeat: repeat-x;
// plus/minus commands
.task-action-btn
display: inline-block
width: 2.12765em
height: 2.12765em
padding: 0
font-size: 1.41em
line-height: 2.12765
text-align: center
color: #222
vertical-align: top
border-right: 1px solid rgba(0,0,0,0.25)
&:last-child
border: 0
&:hover, &:focus
color: #222
text-decoration: none
// checkbox
.task-checker input[type=checkbox]
margin: 0
padding: 0
visibility: hidden
.task-checker label
display: inline-block
width: 3em
height: 3em
margin: 0
vertical-align: top
cursor: pointer
// plus/minus checkbox
.action-plusminus
label
margin-right: 1.5em
label:after
font-size: 1.41em
width: 2.12765em
height: 2.12765em
line-height: 2.12765
text-align: center
display: inline-block
vertical-align top
opacity: 0.3
background-color: #ddd
outline: 1px solid rgba(0,0,0,0.5)
color: #222
label[for$="plus"]:after
content: ''
label[for$="minus"]:after
content: ''
&.select-toggle input[type=checkbox]:checked + label:after
opacity: 1
// yes/no checkbox
.action-yesno
position: relative
// uncompleted icon
label:after, label:before
// content: ''
content: ''
display: block
height: 1.714285714em
width: 1.714285714em
font-size: 1.75em
line-height: 1.714285714em
text-align: center
color: black
opacity: 0.2
label:after
content: ''
label:before
position: absolute
left: 0
// hint at completed icon
label:hover:before,
label:focus:before
content: ''
label:hover:after,
label:focus:after
content: '¬'
font-size: 3.2em
line-height: 0.9375em
height: 0.9375em
width: 1.15em
text-align: center
transform: rotate(135deg)
opacity: 0.5 !important
label:active:after
opacity: 0.75 !important
// completed icon
input[type=checkbox]:checked + label:after
content: '¬'
font-size: 3.2em
line-height: 0.9375em
height: 0.9375em
width: 1.15em
text-align: center
transform: rotate(135deg)
opacity: 0.75
// secondary task commands
// -----------------------
.task-meta-controls
float: right
margin-left: 0.25em
opacity: 0.25
a > i
margin-left: 0.125em
.task-notes
margin-left: 0.125em
.task:hover .task-meta-controls
opacity: 1
// task editing
// ------------
.task-options
padding: 0 0.5em
margin-top: 1.25em
color: #333
.option-group
border-bottom: 1px solid rgba(0,0,0,0.1)
padding: 0 0 1em
margin-bottom: 1em
.option-title
font-size: 1em
margin: 0 0 0.5em
line-height: 1.5
border: 0
padding: 0
color: #333
font-style: italic
&:not(.mega):after
content: ':'
&.mega
cursor: pointer
font-style: normal
font-weight: bold
text-decoration: underline
.option-content
height: 2.5em
width: 100%
margin: 0 0 1em
padding: 0 0 0 0.5em
outline: 0
border: 1px solid rgba(0,0,0,0.2)
box-shadow: none
border-radius: 0
font-family: 'Lato', sans-serif //bootstrap override
textarea.option-content
height: 5em
padding-top: 0.25em
resize: none
margin-bottom: 0
// button-group
.task-controls.tile-group
display: block
text-align: center
margin: 0
.task-action-btn.tile
border: 0
font-size: 1.15em
font-weight: 300
outline: 1px solid rgba(0,0,0,0.2)
outline-offset: -1px
margin: 0 0 0 3px
font-family: 'Lato', sans-serif
text-align: inherit
opacity: 0.5
width: auto
padding: 0 0.5em
&.active
opacity: 1
.tile.spacious
margin: 0.75em 0
font-size: 1.5em
opacity: 1
.tile.bright
background-color: lighten($best, 20%)
&:hover, &:focus
background-color: lighten($best, 40%)
.tile.flush
margin-left: 0
border: 1px solid rgba(0,0,0,0.2)
outline: 0
line-height: 2em
&:first-child
border-right: 0
// todos ui
// --------
// context switching
.context-enabled
.display-context-dependant,
.task-list > li
@ -50,39 +362,19 @@ label.checkbox.inline{
.show-for-uncompleted, .uncompleted
display: block
.help-icon
float:right;
.task:hover
cursor: move
li:hover .task-meta-controls .hover-show
display: inline
.task
list-style:none
pre
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
font-size: 13px
line-height: 18px
color: black
word-break: normal
.task-meta-controls
float:right
i
margin-left:5px
.task-controls,.task-text
display:inline
margin-right:10px
.task-meta-controls .hover-show
display: none;
.vote-up, .vote-down
text-decoration:none;
.new-task-form
margin-bottom:5px;
// nav tabs
.nav-tabs
margin-top: 1.5em
.nav-tabs > li
&.active > a
color: #333
&:hover
margin-top: 1px
> a
border-radius: 0 !important
margin-top: 1px
color: #333
&:hover
border-top: 0
margin-top: 2px

273
test/api.mocha.coffee Normal file
View file

@ -0,0 +1,273 @@
_ = require 'underscore'
request = require 'superagent'
expect = require 'expect.js'
require 'coffee-script'
conf = require("nconf")
conf.argv().env().file({file: __dirname + '../config.json'}).defaults
# Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf)
#FIXME can't get nconf file above to load...
process.env.BASE_URL = conf.get("BASE_URL")
process.env.FACEBOOK_KEY = conf.get("FACEBOOK_KEY")
process.env.FACEBOOK_SECRET = conf.get("FACEBOOK_SECRET")
process.env.NODE_DB_URI = 'mongodb://localhost/habitrpg'
## monkey-patch expect.js for better diffs on mocha
## see: https://github.com/LearnBoost/expect.js/pull/34
origBe = expect.Assertion::be
expect.Assertion::be = expect.Assertion::equal = (obj) ->
@_expected = obj
origBe.call this, obj
# Custom modules
character = require '../src/app/character'
###### Helpers & Variables ######
model = null
uuid = null
taskPath = null
baseURL = 'http://localhost:1337/api/v1'
###### Specs ######
describe 'API', ->
server = null
store = null
model = null
user = null
uid = null
before (done) ->
server = require '../src/server'
server.listen '1337', '0.0.0.0'
server.on 'listening', (data) ->
store = server.habitStore
#store.flush()
model = store.createModel()
model.set '_userId', uid = model.id()
user = character.newUserObject()
user.apiToken = model.id()
model.session = {userId:uid}
model.set "users.#{uid}", user
delete model.session
# Crappy hack to let server start before tests run
setTimeout done, 2000
describe 'Without token or user id', ->
it '/api/v1/status', (done) ->
request.get("#{baseURL}/status")
.set('Accept', 'application/json')
.end (res) ->
expect(res.statusCode).to.be 200
expect(res.body.status).to.be 'up'
done()
it '/api/v1/user', (done) ->
request.get("#{baseURL}/user")
.set('Accept', 'application/json')
.end (res) ->
expect(res.statusCode).to.be 401
expect(res.body.err).to.be 'You must include a token and uid (user id) in your request'
done()
describe 'With token and user id', ->
params = null
currentUser = null
before ->
user = model.at("users.#{uid}")
currentUser = user.get()
params =
title: 'Title'
text: 'Text'
type: 'habit'
beforeEach ->
currentUser = user.get()
it 'GET /api/v1/user', (done) ->
request.get("#{baseURL}/user")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.end (res) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
expect(res.body.id).not.to.be.empty()
self = _.clone(currentUser)
delete self.apiToken
self.stats.toNextLevel = 150
self.stats.maxHealth = 50
expect(res.body).to.eql self
done()
it 'GET /api/v1/user/task/:id', (done) ->
tid = _.pluck(currentUser.tasks, 'id')[0]
request.get("#{baseURL}/user/task/#{tid}")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.end (res) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
expect(res.body).to.eql currentUser.tasks[tid]
done()
it 'POST /api/v1/user/task', (done) ->
request.post("#{baseURL}/user/task")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.send(params)
.end (res) ->
query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
query.fetch (err, user) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 201
expect(res.body.id).not.to.be.empty()
# Ensure that user owns the newly created object
expect(user.get().tasks[res.body.id]).to.be.an('object')
done()
it 'POST /api/v1/user/task (without type)', (done) ->
request.post("#{baseURL}/user/task")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.send({})
.end (res) ->
expect(res.body.err).to.be 'type must be habit, todo, daily, or reward'
expect(res.statusCode).to.be 400
done()
it 'POST /api/v1/user/task (only type)', (done) ->
request.post("#{baseURL}/user/task")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.send(type: 'habit')
.end (res) ->
query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
query.fetch (err, user) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 201
expect(res.body.id).not.to.be.empty()
# Ensure that user owns the newly created object
expect(user.get().tasks[res.body.id]).to.be.an('object')
done()
it 'PUT /api/v1/user/task/:id', (done) ->
tid = _.pluck(currentUser.tasks, 'id')[0]
request.put("#{baseURL}/user/task/#{tid}")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.send(text: 'bye')
.end (res) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
currentUser.tasks[tid].text = 'bye'
expect(res.body).to.eql currentUser.tasks[tid]
done()
it 'PUT /api/v1/user/task/:id (shouldnt update type)', (done) ->
tid = _.pluck(currentUser.tasks, 'id')[1]
type = if currentUser.tasks[tid].type is 'habit' then 'daily' else 'habit'
request.put("#{baseURL}/user/task/#{tid}")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.send(type: type, text: 'fishman')
.end (res) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
currentUser.tasks[tid].text = 'fishman'
expect(res.body).to.eql currentUser.tasks[tid]
done()
it 'PUT /api/v1/user/task/:id (update notes)', (done) ->
tid = _.pluck(currentUser.tasks, 'id')[2]
request.put("#{baseURL}/user/task/#{tid}")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.send(text: 'hi',notes:'foobar matey')
.end (res) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
currentUser.tasks[tid].text = 'hi'
currentUser.tasks[tid].notes = 'foobar matey'
expect(res.body).to.eql currentUser.tasks[tid]
done()
it 'GET /api/v1/user/tasks', (done) ->
request.get("#{baseURL}/user/tasks")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.end (res) ->
query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
query.fetch (err, user) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
model.ref '_user', user
tasks = []
for type in ['habit','todo','daily','reward']
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
tasks = tasks.concat model.get("_#{type}List")
# Ensure that user owns the tasks
expect(res.body.length).to.equal tasks.length
# Ensure that the two sets are equal
expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0
done()
it 'GET /api/v1/user/tasks (todos)', (done) ->
request.get("#{baseURL}/user/tasks")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.query(type:'todo')
.end (res) ->
query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
query.fetch (err, user) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 200
model.ref '_user', user
model.refList "_todoList", "_user.tasks", "_user.todoIds"
tasks = model.get("_todoList")
# Ensure that user owns the tasks
expect(res.body.length).to.equal tasks.length
# Ensure that the two sets are equal
expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0
done()
it 'DELETE /api/v1/user/task/:id', (done) ->
tid = currentUser.habitIds[2]
request.del("#{baseURL}/user/task/#{tid}")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.end (res) ->
expect(res.body.err).to.be undefined
expect(res.statusCode).to.be 204
query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
query.fetch (err, user) ->
expect(user.get('habitIds').indexOf(tid)).to.be -1
expect(user.get("tasks.#{tid}")).to.be undefined
done()
it 'DELETE /api/v1/user/task/:id (no task found)', (done) ->
tid = "adsfasdfjunkshouldntbeatask"
request.del("#{baseURL}/user/task/#{tid}")
.set('Accept', 'application/json')
.set('X-API-User', currentUser.id)
.set('X-API-Key', currentUser.apiToken)
.end (res) ->
expect(res.statusCode).to.be 400
expect(res.body.err).to.be 'No task found.'
done()

View file

@ -12,8 +12,8 @@ casper.start "#{url}/?play=1", ->
@fill 'form#derby-auth-register',
username: user1.id
email: "{user1.id}@gmail.com"
'email-confirmation': "{user1.id}@gmail.com"
password: 'habitrpg123'
'password-confirmation': "habitrpg123"
, true
casper.thenOpen "#{url}/logout"
casper.thenOpen "#{url}/?play=1", ->

View file

@ -1,6 +1,7 @@
--colors
--reporter spec
--timeout 1200
--timeout 2800
--ignore-leaks
--growl
--debug
--compilers coffee:coffee-script

View file

@ -1,13 +1,14 @@
{expect} = require 'derby/node_modules/racer/test/util'
{BrowserModel: Model} = require 'derby/node_modules/racer/test/util/model'
derby = require 'derby'
clone = require 'clone'
lodash = require 'lodash'
_ = require 'underscore'
moment = require 'moment'
# Custom modules
scoring = require '../src/app/scoring'
schema = require '../src/app/schema'
schema = require '../src/app/character'
helpers = require '../src/app/helpers'
###### Helpers & Variables ######
@ -19,8 +20,8 @@ taskPath = null
# Otherwise, using model.get(path) will give the same object before as after
pathSnapshots = (paths) ->
if _.isString(paths)
return clone(model.get(paths))
_.map paths, (path) -> clone(model.get(path))
return lodash.cloneDeep(model.get(paths))
_.map paths, (path) -> lodash.cloneDeep(model.get(path))
statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task
cleanUserObj = ->
@ -67,6 +68,18 @@ modificationsLookup = (direction, options = {}) ->
###### Specs ######
describe 'Cron', ->
it 'calculates day differences with dayStart properly', ->
dayStart = 4
yesterday = moment().subtract('d', 1).add('h', dayStart)
now = moment().startOf('day').add('h', dayStart-1) #today
console.log {yesterday: yesterday.format('MM/DD HH:00'), now: now.format('MM/DD HH:00')}
console.log {diff: Math.abs(moment(yesterday).diff(moment(now), 'days'))}
expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 0
now = moment().startOf('day').add('h', dayStart)
console.log {now: now.format('MM/DD HH:00')}
expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 1
describe 'User', ->
model = null

View file

@ -1,6 +1,32 @@
<alerts:>
{#if _flash.error}
<ul class="alert alert-error">
<ul class="unstyled alert alert-error">
{#each _flash.error as :error}<li>{:error}</li>{/}
</ul>
{/}
{#if equal(_user.flags.algosNotification,'show')}
<div class='alert alert-success'>
<a x-bind="click:closeAlgosNotification" class=pull-right>[x]</a>
<p>New features! Custom day start, new and better algorithms, authentication enhancements. See <a target="_blank" href="http://habitrpg.tumblr.com/post/44468869138/custom-day-start-better-algorithms-auth-enhancements">details here.</a>
&nbsp;"Hey, I had more Exp before this roll-out!" <a data-target="#restore-modal" data-toggle=modal>Restore your Exp here.</a></p>
</div>
{/}
{#if equal(_user.flags.onliesNotification, 'show')}
<div class='alert alert-success'>
<a x-bind="click:closeOnliesNotification " class=pull-right>[x]</a>
<p>
"Onlies" now gain / lose value just like other habits, and are reset back to 0 at the beginning of each day.
&nbsp;See <a target="_blank" href="https://trello.com/card/shading-onlies/50e5d3684fe3a7266b0036d6/67">details here</a>, and
&nbsp;chime in with your recommendations on this mechanic.
</p>
</div>
{/}
{#if equal(_user.flags.priorityNotification, 'show')}
<div class='alert alert-success'>
<a x-bind="click:closePriorityNotification" class=pull-right>[x]</a>
<p>New Feature: Priority Multiplier! You can now multiply "more important" tasks on a 1x, 1.5x, or 2x scale. <a target="_blank" href="https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17">See details</a>.</p>
</div>
{/}

View file

@ -1,20 +1,18 @@
<modals:>
<app:modals:modal modalId="avatar-modal">
{#if _profileActiveMain}
{#with _user.preferences}
<ul class="nav nav-tabs">
<li class="active"><a data-toggle='tab' href="#profileCustomize">Avatar</a></li>
<li><a data-toggle='tab' href="#profileEdit">Profile</a></li>
</ul>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle='tab' href="#profileCustomize">Avatar</a></li>
<li><a data-toggle='tab' href="#profileEdit">Profile</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="profileCustomize">
<app:avatar:customize />
</div>
<div class="tab-pane" id="profileEdit"><app:avatar:profile user="{_user}" main="true" /></div>
<div class="tab-content">
<div class="tab-pane active" id="profileCustomize">
<app:avatar:customize />
</div>
{/}
<div class="tab-pane" id="profileEdit"><app:avatar:profile user="{_user}" main="true" /></div>
</div>
{else}
<app:avatar:profile profile={_profileActive} />
{/}
@ -25,19 +23,28 @@
<avatar:>
<div class="avatar {{#if @main}}main-avatar{{/}} {{#if @party}}party-avatar{{/}}" data-toggle='modal' data-target='#avatar-modal' data-uid="{@profile.id}" x-bind="click:profileChangeActive">
{#unless @minimal}{#if @profile.items.pet}<img src='img/sprites/{@profile.items.pet.icon}' />{/}{/}
<div class='character-sprites'>
{#with @profile.preferences as :p}
<span class='{:p.gender}_skin_{:p.skin}'></span>
<span class='{:p.gender}_hair_{:p.hair}'></span>
<span class="{equipped(@profile, 'armor')}"></span>
<span class="{equipped(@profile, 'head')}"></span>
<span class='{:p.gender}_shield_{@profile.items.shield}'></span>
<span class='{:p.gender}_weapon_{@profile.items.weapon}'></span>
{/}
</div>
{#if and(@profile.items.pet, not(@minimal))}
<img src='/img/sprites/{@profile.items.pet.icon}' />
{/}
{#with @profile.preferences as :p}
<div class='character-sprites'>
<span class='{:p.gender}_skin_{:p.skin}'></span>
<span class='{:p.gender}_hair_{:p.hair}'></span>
<span class="{equipped(@profile, 'armor')}"></span>
{#if :p.showHelm}
<span class="{equipped(@profile, 'head')}"></span>
{else}
<span class="{:p.gender}_head_0"></span>
{/}
<span class='{:p.gender}_shield_{@profile.items.shield}'></span>
<span class='{:p.gender}_weapon_{@profile.items.weapon}'></span>
</div>
{/}
{{#unless @minimal}}
<div class="lvl"><span class="badge badge-info">Lvl {@profile.stats.lvl}</span></div>
<div class="lvl"><span class="badge badge-info">Lvl {@profile.stats.lvl}</span></div>
{{/}}
</div>
@ -49,97 +56,103 @@
</figure>
<!-- customization options -->
<menu type="list">
<!-- gender -->
<li class="customize-menu">
<menu label="Gender">
<button type="button" class="m_head_0 customize-option" data-value="m" x-bind="click:customizeGender"></button>
<button type="button" class="f_head_0 customize-option" data-value="f" x-bind="click:customizeGender"></button>
</menu>
</li>
<!-- hair -->
<li class="customize-menu">
<menu label="Hair">
<button type="button" class="{.gender}_hair_blond customize-option" data-value="blond" x-bind="click:customizeHair"></button>
<button type="button" class="{.gender}_hair_black customize-option" data-value="black" x-bind="click:customizeHair"></button>
<button type="button" class="{.gender}_hair_brown customize-option" data-value="brown" x-bind="click:customizeHair"></button>
<button type="button" class="{.gender}_hair_white customize-option" data-value="white" x-bind="click:customizeHair"></button>
</menu>
</li>
<!-- skin -->
<li class="customize-menu">
<menu label="Skin">
<button type="button" class='{.gender}_skin_dead customize-option' data-value="dead" x-bind="click:customizeSkin"></button>
<button type="button" class='{.gender}_skin_orc customize-option' data-value="orc" x-bind="click:customizeSkin"></button>
<button type="button" class='{.gender}_skin_asian customize-option' data-value="asian" x-bind="click:customizeSkin"></button>
<button type="button" class='{.gender}_skin_black customize-option' data-value="black" x-bind="click:customizeSkin"></button>
<button type="button" class='{.gender}_skin_white customize-option' data-value="white" x-bind="click:customizeSkin"></button>
</menu>
</li>
</menu>
{#if equal(_user.preferences.gender, 'f')} <!-- Sorry boys -->
{#with _user.preferences as :p}
<menu type="list">
<!-- gender -->
<li class="customize-menu">
<menu label="Clothing">
<button type="button" class="f_armor_0_v1 customize-option" data-value="v1" x-bind="click:customizeArmorSet"></button>
<button type="button" class="f_armor_0_v2 customize-option" data-value="v2" x-bind="click:customizeArmorSet"></button>
<menu label="Head">
<button type="button" class="m_head_0 customize-option" data-value="m" x-bind="click:customizeGender"></button>
<button type="button" class="f_head_0 customize-option" data-value="f" x-bind="click:customizeGender"></button>
</menu>
<label class="checkbox">
<input type=checkbox checked="{_user.preferences.showHelm}" /> Show Helm
</label>
</li>
<!-- hair -->
<li class="customize-menu">
<menu label="Hair">
<button type="button" class="{:p.gender}_hair_blond customize-option" data-value="blond" x-bind="click:customizeHair"></button>
<button type="button" class="{:p.gender}_hair_black customize-option" data-value="black" x-bind="click:customizeHair"></button>
<button type="button" class="{:p.gender}_hair_brown customize-option" data-value="brown" x-bind="click:customizeHair"></button>
<button type="button" class="{:p.gender}_hair_white customize-option" data-value="white" x-bind="click:customizeHair"></button>
</menu>
</li>
<!-- skin -->
<li class="customize-menu">
<menu label="Skin">
<button type="button" class='{:p.gender}_skin_dead customize-option' data-value="dead" x-bind="click:customizeSkin"></button>
<button type="button" class='{:p.gender}_skin_orc customize-option' data-value="orc" x-bind="click:customizeSkin"></button>
<button type="button" class='{:p.gender}_skin_asian customize-option' data-value="asian" x-bind="click:customizeSkin"></button>
<button type="button" class='{:p.gender}_skin_black customize-option' data-value="black" x-bind="click:customizeSkin"></button>
<button type="button" class='{:p.gender}_skin_white customize-option' data-value="white" x-bind="click:customizeSkin"></button>
</menu>
</li>
</menu>
{#if equal(_user.preferences.gender, 'f')} <!-- Sorry boys -->
<menu type="list">
<li class="customize-menu">
<menu label="Clothing">
<button type="button" class="f_armor_0_v1 customize-option" data-value="v1" x-bind="click:customizeArmorSet"></button>
<button type="button" class="f_armor_0_v2 customize-option" data-value="v2" x-bind="click:customizeArmorSet"></button>
</menu>
</li>
</menu>
{/}
{/}
<profile:>
<div class='row-fluid'>
<div class='span6 well'>
{#if _profileEditing}
{#if _profileActiveMain}
<a class='btn btn-success' x-bind="click:profileSave">Save</a>
<!-- TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak -->
<div class="control-group">
<label class="control-label" for="profileImageUrl">Photo Url</label>
<div class="controls">
<input type="url" id="profileImageUrl" value="{_user.profile.imageUrl}" placeholder="Image Url"/>
</div>
{#if and(_profileEditing, _profileActiveMain)}
<a class='btn btn-success' x-bind="click:profileSave">Save</a>
<!-- TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak -->
<div class="control-group">
<label class="control-label" for="profileImageUrl">Photo Url</label>
<div class="controls">
<input type="url" id="profileImageUrl" value="{_user.profile.imageUrl}" placeholder="Image Url"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="profileFullName">Full Name</label>
<div class="controls">
<input type="text" id="profileFullName" placeholder="Full Name" value="{_user.profile.name}" />
</div>
<div class="control-group">
<label class="control-label" for="profileFullName">Full Name</label>
<div class="controls">
<input type="text" id="profileFullName" placeholder="Full Name" value="{_user.profile.name}" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="profileBlurb">Blurb</label>
<div class="controls">
<textarea id="profileBlurb" placeholder="Blurb" >{_user.profile.blurb}</textarea>
</div>
<div class="control-group">
<label class="control-label" for="profileBlurb">Blurb</label>
<div class="controls">
<textarea id="profileBlurb" placeholder="Blurb" >{_user.profile.blurb}</textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="profileWebsite">Websites</label>
<div class="controls">
<form x-bind="submit:profileAddWebsite">
<input type="url" id="profileWebsite" value="{_newProfileWebsite}" placeholder="Add Website"/>
</form>
</div>
<ul>
{#each _user.profile.websites as :website}
<!-- would prefer if there were and index in #each, instead using data-website to search with indexOf -->
<li>{:website} <a data-website="{:website}" x-bind="click:profileRemoveWebsite"><i class='icon-remove'></i></a></li>
{/}
</ul>
<div class="control-group">
<label class="control-label" for="profileWebsite">Websites</label>
<div class="controls">
<form x-bind="submit:profileAddWebsite">
<input type="url" id="profileWebsite" value="{_newProfileWebsite}" placeholder="Add Website"/>
</form>
</div>
<a class='btn btn-success' x-bind="click:profileSave">Save</a>
{/}
<ul>
{#each _user.profile.websites as :website}
<!-- would prefer if there were and index in #each, instead using data-website to search with indexOf -->
<li>{:website} <a data-website="{:website}" x-bind="click:profileRemoveWebsite"><i class='icon-remove'></i></a></li>
{/}
</ul>
</div>
<a class='btn btn-success' x-bind="click:profileSave">Save</a>
{else}
<h3>
{#if _profileActive.profile.name}{_profileActive.profile.name}
{else}{username(_profileActive.auth)}
{#if _profileActive.profile.name}
{_profileActive.profile.name}
{else}
{username(_profileActive.auth)}
{/}
{#if _profileActiveMain}<a class='btn pull-right' x-bind="click:profileEdit">Edit</a>{/}
</h3>
@ -152,7 +165,7 @@
{#if _profileActive.profile.websites}
<ul>
{#each _profileActive.profile.websites as :website}
<li>{:website}</li>
<li>{:website}</li>
{/}
</ul>
{/}

View file

@ -1,41 +1,71 @@
<footer:>
<footer class=footer>
<div class=container>
{#if equal(_view.nodeEnv, 'production')}
<table class=pull-right><tr>
<td><!-- Github -->
<iframe src="/vendor/github-buttons/github-btn.html?user=lefnire&repo=habitrpg&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="85px" height="20px"></iframe>
</td>
<td>
<div class="addthis_toolbox addthis_default_style "
addthis:url="https://habitrpg.com"
addthis:title="HabitRPG - Gamify Your Life">
<a class="addthis_button_facebook_like" fb:like:layout="button_count"></a>
<a class="addthis_button_tweet" tw:via="habitrpg"></a>
</div>
</td>
</tr></table>
{else}
<div class='pull-right'>
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
<button class='btn' x-bind="click:cheat">Add GP & Exp</button>
</div>
{/}
<div>
<ul>
<li>Copyright &copy; 2012 OCDevel LLC</li>
<li><a href="/splash.html">About</a></li>
<li><a target="_blank" href="http://habitrpg.tumblr.com/">Blog</a></li>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg/issues?state=open">Bugs</a></li>
<li><a target="_blank" href="https://trello.com/board/habitrpg/50e5d3684fe3a7266b0036d6">Features</a></li>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg/wiki/FAQ">FAQ</a></li>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg/wiki/API">API</a></li>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg/wiki/Add-ons-and-Extensions">Add-ons / Extensions</a></li>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg#contact">Contact</a></li>
<li><a href="/privacy">Privacy</a></li>
<li><a href="/terms">Terms</a></li>
</ul>
<div class='row'>
<div class='span3'>
<h4>Company</h4>
<ul class='unstyled'>
<li><a href="/static/about">About</a></li>
<li><a target="_blank" href="http://habitrpg.tumblr.com/">Blog</a></li>
<li><a href="/static/team">Team</a></li>
<li><a href="/static/extensions">Extensions</a></li>
<li><a href="/static/faq">FAQ</a></li>
</ul>
</div>
<div class='span3'>
<h4>Legal</h4>
<ul class='unstyled'>
<li><a href="/static/privacy">Privacy</a></li>
<li><a href="/static/terms">Terms</a></li>
<li><strong>&copy; 2012 OCDevel LLC</strong></li>
</ul>
</div>
<div class='span3'>
<h4>Community</h4>
<ul class='unstyled'>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg/issues?state=open">Submit Bug</a></li>
<li><a target="_blank" href="https://trello.com/board/habitrpg/50e5d3684fe3a7266b0036d6">Request Feature</a></li>
<li><a target="_blank" href="https://github.com/lefnire/habitrpg/wiki/API">API</a></li>
<li><a href="/static/extensions">Add-ons / Extensions</a></li>
<li><a target="_blank" href="http://www.kickstarter.com/projects/lefnire/habitrpg-mobile">Kickstarter</a></li>
<li><a target="_blank" href="https://www.facebook.com/Habitrpg">Facebook Page</a></li>
<li><a target="_blank" href="http://www.reddit.com/r/habitrpg/">Reddit</a></li>
</ul>
</div>
<div class='span3'>
{{#if equal(_view.nodeEnv, 'development')}}
<h4>Cheat</h4>
<ul class='unstyled'>
<li><button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button></li>
<li><button class='btn' x-bind="click:emulateTenDays">Emulate 10 Days</button></li>
<li><button class='btn' x-bind="click:cheat">Insta Level</button></li>
<li><button class='btn' x-bind='click:reset'>Reset Level</button></li>
</ul>
{{else}}
<h4>Social</h4>
<div class="addthis_toolbox addthis_default_style "
addthis:url="https://habitrpg.com"
addthis:title="HabitRPG - Gamify Your Life">
<table>
<tr><td><a class="addthis_button_facebook_like" fb:like:layout="button_count"></a></td></tr>
<tr><td><a class="addthis_button_tweet" tw:via="habitrpg"></a></td></tr>
<tr><td align='left'>
<iframe src="/vendor/github-buttons/github-btn.html?user=lefnire&repo=habitrpg&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="85px" height="20px" ></iframe>
</td></tr>
<tr><td><a class="addthis_button_google_plusone" g:plusone:size="medium"></a> </td></tr>
</table>
</div>
{{/}}
</div>
</div>
</div>
</footer>

View file

@ -1,40 +1,58 @@
<header:>
<div class="row-fluid">
<div class='char-status {#if gt(_partyMembers.length,1)}span8 offset2 has-party{else}span6 offset3{/}'>
{#unless equal(_user.preferences.hideHeader,true)}
<div class="row-fluid">
<div class='char-status {#if gt(_partyMembers.length,1)}span8 offset2 has-party{else}span6 offset3{/}'>
<!-- avatar -->
<figure class="main-avatar-wrap">
<app:avatar:avatar profile={_user} main="true" />
</figure>
<!-- avatar -->
<figure class="main-avatar-wrap">
<app:avatar:avatar profile={_user} main="true" />
</figure>
<!-- party -->
{#each _partyMembers as :member}
{#unless equal(:member.id, _userId)}
<figure class="party-avatar-wrap" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom">
<app:avatar:avatar profile={:member} party="true" />
</figure>
{/}
{/}
<!-- party -->
{{#each _partyMembers as :member}}
<!-- #each requires an element, so #if / #unless blocks nested immediately inside cause issues if they return false -->
<!-- see https://github.com/codeparty/derby/issues/215 -->
<span>
{{#unless equal(:member.id, _userId)}}
<figure class="party-avatar-wrap" rel="popover" data-title="{{username(:member.auth)}}" data-placement="bottom" data-trigger="hover" data-html="true" data-content="
<div>
<div class='progress progress-danger' style='height:5px;'>
<div class='bar' style='height: 5px; width: {percent(:member.stats.hp, 50)}%;'></div>
</div>
<div class='progress progress-warning' style='height:5px;'>
<div class='bar' style='height: 5px; width: {percent(:member.stats.exp, tnl(:member.stats.lvl))}%;'></div>
</div>
<div>GP: {{floor(:member.stats.gp)}}</div>
</div>
">
<!-- Would be way cleaner as a Derby template `data-content="<app:party:member-stats profile={{:member}} />"`, but it was just printing HTML as text -->
<!-- progress bars -->
<div class="progress-bars">
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
<app:avatar:avatar profile={{:member}} party="true" />
</figure>
{{/}}
</span>
{{/}}
<!-- progress bars -->
<div class="progress-bars">
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
<span class="progress-text"><i class=icon-heart></i> {ceil(_user.stats.hp)} / 50</span>
</div>
<div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience">
<div class="bar" style="width: {percent(_user.stats.exp,tnl(_user.stats.lvl))}%;"></div>
<span class="progress-text">
{#if _user.history.exp}
<a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>&nbsp;
{/}
<i class=icon-star></i> {floor(_user.stats.exp)} / {tnl(_user.stats.lvl)}
</span>
</div>
<div id="exp-chart" style="display:none;"></div>
</div>
<div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience">
<div class="bar" style="width: {percent(_user.stats.exp,_tnl)}%;"></div>
<span class="progress-text">
{#if _user.history.exp}
<a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>&nbsp;
{/}
<i class=icon-star></i> {round(_user.stats.exp)} / {_tnl}
</span>
</div>
<div id="exp-chart" style="display:none;"></div>
<!-- end .char-status -->
</div>
<!-- end .char-status -->
<!-- end .row-fluid -->
</div>
<!-- end .row-fluid -->
</div>
{/}

View file

@ -1,9 +1,9 @@
<import: src="modals">
<import: src="tasks" ns="">
<import: src="tasks">
<import: src="header">
<import: src="alerts">
<import: src="avatar">
<import: src="rewards" ns="">
<import: src="rewards">
<import: src="footer">
<import: src="settings">
<import: src="party">
@ -13,13 +13,23 @@
HabitRPG | Gamify Your Life
<Head:>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- webfonts -->
<link href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic' rel='stylesheet' type='text/css'>
<!-- CDN -->
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap.min.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap-responsive.min.css" rel="stylesheet">
<Header:>
<app:modals:modals />
<ui:connectionAlert>
<div id="head" class="container-fluid">
{#if _undo}<a x-bind="click:undo" class='label undo-button'>Undo</a>{/}
<app:settings:menu />
<app:header:header />
</div>
@ -27,25 +37,36 @@
<Body:>
<br/>
<div id="notification-area"></div>
<div id=wrap class="container-fluid">
<div id="wrap">
<app:alerts:alerts />
<app:filters:filters />
<div id=main class="row-fluid">
<app:taskLists />
<div id=main class="grid">
<app:tasks:taskLists />
</div>
</div>
<app:footer:footer />
<Scripts:>
<!-- scripts go in /src/app/browser.coffee, that way we can get min/concat -->
<!-- only the ones that are remote and can't be done async are here -->
<!-- we load as many CDN-available javascript files here as possible. For the rest,
we use /src/app/browser.coffee -> loadJavaScripts() which concats/minifies /public/vendor javascript libraries,
as well as runs async loads -->
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.3.1/jquery.cookie.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-growl/1.0.0/jquery.bootstrap-growl.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.0.0/js/bootstrap-datepicker.min.js"></script>
<script src="https://checkout.stripe.com/v2/checkout.js"></script>
{#if equal(_view.nodeEnv,"production")}
<!-- Google Analytics -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-33510635-1']);
_gaq.push(['_setDomainName', 'habitrpg.com']);
_gaq.push(['_trackPageview']);
(function() {

View file

@ -17,7 +17,7 @@
<!-- Re-Roll modal -->
<app:modals:modal modalId='reroll-modal' header="Reset Your Tasks">
<app:userTokens/>
<app:rewards:userTokens/>
<p>Highly discouraged because red tasks provide good incentive to improve (<a target="_blank" href="https://github.com/lefnire/habitrpg#all-my-tasks-are-red-im-dying-too-fast">read more</a>). However, this becomes necessary after long bouts of bad habits.</p>
<@footer>
{#if lt(_user.balance,1)}
@ -30,7 +30,7 @@
<!-- Buy more tokens modal -->
<app:modals:modal modalId='more-tokens-modal' header="Out Of Tokens">
<app:userTokens/>
<app:rewards:userTokens/>
<p>You're out of tokens, which are used to buy pets and mounts.</p>
<@footer>
<a data-dismiss="modal" x-bind="click:showStripe" class="btn btn-success btn-large">Buy More Tokens</a><span class='token-cost'>Not enough tokens</span>

View file

@ -1,65 +1,142 @@
<rewardsColumn:>
<div class="module">
<div class="task-column rewards">
<!-- cash or tokens -->
<span class='option-box pull-right wallet'>
{#unless _view.activeTabPets}
<div class="money">
{gold(_user.stats.gp)}
<span class='shop_gold' rel='tooltip' title='Gold'></span>
</div>
<div class="money">
{silver(_user.stats.gp)}
<span class='shop_silver' rel='tooltip' title='Silver'></span>
</div>
<!-- <div class="money">
{copper(_user.stats.gp)}
<span class='shop_copper' rel='tooltip' title='Copper'></span>
</div> -->
{else}
<app:rewards:userTokens />
{/}
</span>
<h2 class="task-column_title">Rewards</h2>
<!-- if pets are enabled, make tabs -->
{#if equal(_user.flags.petsEnabled,true)}
<div class='tabbable tabs-below'>
<div class="tab-content">
<div class="tab-pane active" id="rewards-tab">
<app:rewards:rewardsTab />
</div>
<!-- pets pane -->
<div class="tab-pane pet-grid" id="pets-tab">
{#with _view.items.pets as :pets}
<table>
<tr>
{#with :pets[0]}<app:rewards:pet />{/}
{#with :pets[1]}<app:rewards:pet />{/}
{#with :pets[2]}<app:rewards:pet />{/}
{#with :pets[3]}<app:rewards:pet />{/}
</tr>
<tr>
{#with :pets[4]}<app:rewards:pet />{/}
{#with :pets[5]}<app:rewards:pet />{/}
{#with :pets[6]}<app:rewards:pet />{/}
{#with :pets[7]}<app:rewards:pet />{/}
</tr>
<tr>
{#with :pets[8]}<app:rewards:pet />{/}
{#with :pets[9]}<app:rewards:pet />{/}
{#with :pets[10]}<app:rewards:pet />{/}
{#with :pets[11]}<app:rewards:pet />{/}
</tr>
<tr>
{#with :pets[12]}<app:rewards:pet />{/}
{#with :pets[13]}<app:rewards:pet />{/}
{#with :pets[14]}<app:rewards:pet />{/}
{#with :pets[15]}<app:rewards:pet />{/}
</tr>
<tr>
{#with :pets[16]}<app:rewards:pet />{/}
</tr>
</table>
{/}
</div>
<!--<div class="tab-pane" id="mounts-tab">...</div>-->
</div>
<ul class="nav nav-tabs" id="rewardsTabs">
<li class="active"><a href="#rewards-tab" data-toggle="tab" x-bind="click:activateRewardsTab">Rewards</a></li>
<li><a href="#pets-tab" data-toggle="tab" x-bind='click:activatePetsTab'>Pets</a></li>
<!--<li><a href="#mounts-tab" data-toggle="tab">Mounts</a></li>-->
</ul>
</div>
<!-- if pets are disabled, just do rewards tab -->
{else}
<app:rewards:rewardsTab />
{/}
</div>
</div>
<rewardsTab:>
<!-- add new reward -->
<app:tasks:newTask type="reward" inputValue="{_newReward}" placeHolder="New Reward" />
{#if _rewardList}
<ul class='rewards'>
{#each _rewardList as :task}<app:task />{/}
{#each _rewardList as :task}<app:tasks:task />{/}
</ul>
{/}
{#if _user.flags.itemsEnabled}
<ul class='items'>
{#with _view.items.weapon as :item}<app:item />{/}
{#with _view.items.armor as :item}<app:item />{/}
{#with _view.items.head as :item}<app:item />{/}
{#with _view.items.shield as :item}<app:item />{/}
{#with _view.items.potion as :item}<app:item />{/}
{#with _view.items.reroll as :item}<app:item reroll=true />{/}
{#with _view.items.weapon as :item}<app:rewards:item />{/}
{#with _view.items.armor as :item}<app:rewards:item />{/}
{#with _view.items.head as :item}<app:rewards:item />{/}
{#with _view.items.shield as :item}<app:rewards:item />{/}
{#with _view.items.potion as :item}<app:rewards:item />{/}
{#with _view.items.reroll as :item}<app:rewards:item reroll=true />{/}
</ul>
{/}
<pet:>
<div class="span3 shop-pet" rel=tooltip title="{.text} - {.value} Tokens">
<div style='position:relative'>
{#if _user.items.pets[.name]}<i class='icon-ok shop-pet-owned-check'></i>{/}
<img x-bind="click:selectPet" data-pet='{.name}' src='img/sprites/{.icon}'/>
</div>
</div>
<td class="{#if _user.items.pets[.name]}active-pet{/}">
<img rel=tooltip title="{.text} - {.value} Tokens" x-bind="click:selectPet" data-pet='{.name}' src='img/sprites/{.icon}'/>
</td>
<userTokens:>
<div class="pull-right">
<div class="input-append">
<span class="uneditable-input" style="width:auto;">{tokens(_user.balance)}</span>
<span class="add-on">Tokens</span>
</div>
</div>
<!--<span class="well pull-right">Tokens: {tokens(_user.balance)}</span>-->
<a class="pull-right token-wallet">
<span class="task-action-btn tile flush bright add-token-btn"></span>
<span class="task-action-btn tile flush neutral">{tokens(_user.balance)} Tokens</span>
</a>
<item:>
{#unless :item.hide}
<li class="task reward item">
<pre>
<div class="task-meta-controls">
<span rel="popover" data-trigger="hover" data-placement="left" data-content="{:item.notes}" data-original-title="{:item.text}" class='task-notes'><i class="icon-comment"></i></span>
</div>
<div class="task-controls">
{#if @reroll}
<a href="#" data-toggle="modal" data-target="#reroll-modal" class="item-buy-link" style='cursor:pointer'><i class=icon-retweet></i></a>
{else}
<a x-bind=click:buyItem class="item-buy-link" data-type={:item.type} data-value={:item.value} data-index={:item.index}>{:item.value}<img src="/img/coin_single_gold.png"/></a>
{/}
</div>
<div class="task-text">
{#if :item.icon}
<img src="/img/sprites/{:item.icon}" /> {:item.text}
{else}
<table class='shop-table'><tr>
<td><div class="shop_{:item.classes} shop-sprite"></div></td>
<td>{:item.text}</td>
</tr></table>
{/}
<li class="task reward-item {#if :item.hide}hide{/}">
<!-- right-hand side control buttons -->
<div class="task-meta-controls">
<span rel="popover" data-trigger="hover" data-placement="left" data-content="{:item.notes}" data-original-title="{:item.text}" class='task-notes'><i class="icon-comment"></i></span>
</div>
</div>
</pre>
</li>
{/}
<!-- left-hand size commands -->
<div class="task-controls">
{#if @reroll}
<a class="task-action-btn btn-reroll" data-toggle="modal" data-target="#reroll-modal"></a>
{else}
<a class="money btn-buy item-btn" x-bind=click:buyItem data-type={:item.type} data-value={:item.value} data-index={:item.index}>
<span class="reward-cost">{:item.value}</span>
<span class='shop_gold'></span>
</a>
{/}
</div>
<!-- main content -->
<span class="shop_{:item.classes} shop-sprite item-img"></span>
<p class="task-text">{:item.text}</p>
</li>

View file

@ -1,22 +1,48 @@
<modals:>
{{#if _loggedIn}}
<app:modals:modal modalId="settings-modal" header="Settings">
<h4>API</h4>
<small>Copy these for use in third party applications.</small>
<h6>User ID</h6>
<pre class=prettyprint>{_user.id}</pre>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#" data-target="#settings-settings">General</a></li>
<li><a data-toggle="tab" href="#" data-target="#settings-api">API</a></li>
</ul>
<h6>API Token</h6>
<pre class=prettyprint>{_user.apiToken}</pre>
<div class="tab-content">
<div class="tab-pane active" id="settings-settings">
<h4>Custom Day Start</h4>
<div class="input-append">
<input class="span2" type="number" min=0 max=24 value={_user.preferences.dayStart} />
<span class="add-on">:00 (24h)</span>
</div>
<div>
<small>Habit defaults to check and reset your dailies at midnight each day. You can customize that here (Enter number between 0 and 24).</small>
</div>
{{#if _user.auth.local}}
<hr/>
<derby-auth:changePassword />
{{/}}
<hr/>
<h4>Misc</h4>
<button x-bind="click:toggleHeader" class="btn">{#if _user.preferences.hideHeader}Show Header{else}Hide Header{/}</button>
{#unless _user.flags.partyEnabled}&nbsp;<button x-bind="click:manuallyUnlockParties" data-dismiss='modal' rel=tooltip title="Parties are unlocked automatically for usability reasons, but you can override that here.">Enable Parties</button>{/}
<hr/>
<a class='btn btn-danger' data-target="#reset-modal" data-toggle="modal" rel=tooltip title="Resets your entire account (dangerous).">Reset</a>
&nbsp;<a class='btn btn-danger' data-target="#restore-modal" data-toggle="modal" rel=tooltip title="Restores attributes to your character.">Restore</a>
{{#if _user.auth.local}}
<hr/>
<h4>Change Password</h4>
<derby-auth:changePassword />
{{/}}
<hr/>
<h4>Danger Zone</h4>
<a class='btn btn-danger' data-target="#reset-modal" data-toggle="modal" rel=tooltip title="Resets your entire account (dangerous).">Reset</a>
&nbsp;<a class='btn btn-danger' data-target="#restore-modal" data-toggle="modal" rel=tooltip title="Restores attributes to your character.">Restore</a>
&nbsp;<a class='btn btn-danger' data-target="#delete-modal" data-toggle="modal" rel=tooltip title="Delete your account.">Delete</a>
</div>
<div class="tab-pane" id="settings-api">
<small>Copy these for use in third party applications.</small>
<h6>User ID</h6>
<pre class=prettyprint>{_user.id}</pre>
<h6>API Token</h6>
<pre class=prettyprint>{_user.apiToken}</pre>
</div>
</div>
<@footer>
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
@ -81,6 +107,15 @@
</@footer>
</app:modals:modal>
<app:modals:modal modalId="delete-modal" header="Delete Account">
<p>Woa woa woa! Are you sure? This will seriously delete your account forever, and it can never be restored. If you're absolutely certain, type <strong>DELETE</strong> into the text-box below.</p>
<p><input type="text" value="{_deleteAccount}" /></p>
<@footer>
<button class="btn btn-large" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button data-dismiss="modal" x-bind="click:deleteAccount" class="btn btn-danger btn-small {#unless equal(_deleteAccount,'DELETE')}disabled{/}">Delete Account</button>
</@footer>
</app:modals:modal>
{{else}}
<app:modals:modal modalId="login-modal" header="Login / Register">
<a href="/auth/facebook"><img src='/img/facebook-login-register.jpeg' alt="Login / Register With Facebook"/></a>

View file

@ -1,220 +1,197 @@
<taskLists:>
<!--helpTitle & helpContent moved to tour -->
<div class="span3 well habits">
<h2>Habits</h2>
<app:newTask type="habit" inputValue="{_newHabit}" placeHolder="New Habit" />
<ul class="habits">{#each _habitList as :task}<app:task />{/}</ul>
<!-- Habits Column -->
<div class="module">
<div class="task-column habits">
<h2 class="task-column_title">Habits</h2>
<app:tasks:newTask type="habit" inputValue="{_newHabit}" placeHolder="New Habit" />
<ul class="habits">{#each _habitList as :task}<app:tasks:task />{/}</ul>
</div>
</div>
<div class="span3 well dailys">
<h2>Daily</h2>
<app:newTask type="daily" inputValue="{_newDaily}" placeHolder="New Daily" />
<ul class='dailys'>{#each _dailyList as :task}<app:task />{/}</ul>
<!-- Dailys Column -->
<div class="module">
<div class="task-column dailys">
<h2 class="task-column_title">Daily</h2>
<app:tasks:newTask type="daily" inputValue="{_newDaily}" placeHolder="New Daily" />
<ul class='dailys'>{#each _dailyList as :task}<app:tasks:task />{/}</ul>
</div>
</div>
<div class="span3 well todos context-enabled context-uncompleted tabbable tabs-below" id="todo-well">
<h2>Todos</h2>
<div class="display-context-dependant show-for-uncompleted">
<app:newTask type="todo" inputValue="{_newTodo}" placeHolder="New Todo" />
</div>
<ul class='todos task-list'>
{#each _todoList as :task}<app:task />{/}
</ul>
<div class="display-context-dependant show-for-completed">
<a class='btn' x-bind=click:clearCompleted>Clear Completed</a>
<p>&nbsp;</p>
</div>
<span class='pull-right'>
<!-- Todos Column -->
<div class="module">
<div class="task-column todos context-enabled context-uncompleted tabbable tabs-below" id="todo-well">
<!-- todo export/graph options -->
<span class='option-box pull-right'>
{#if _user.history.todos}
<a x-bind=click:toggleChart data-toggle-id="todos-chart" data-history-path="_user.history.todos" rel=tooltip title="Progress"><i class=icon-signal></i></a>
<a class="option-action" x-bind=click:toggleChart data-toggle-id="todos-chart" data-history-path="_user.history.todos" rel=tooltip title="Progress"><i class=icon-signal></i></a>
{/}
&nbsp;<a href="/v1/users/{{_user.id}}/calendar.ics?apiToken={{_user.apiToken}}"><img src='/img/calendar_ical.png' title="iCal" alt="iCal" rel=tooltip /></a>
<!--&nbsp;<a target="_blank" href="https://www.google.com/calendar/render?cid={{encodeiCalLink(_user.id, _user.apiToken)}}"><img src='/img/calendar_google.png' title="Google Calendar" alt="Google Calendar" rel=tooltip /></a>-->
<a class="option-action" href="/v1/users/{{_user.id}}/calendar.ics?apiToken={{_user.apiToken}}" rel=tooltip title="iCal"><i class=icon-calendar></i></a>
<!-- <a href="https://www.google.com/calendar/render?cid={{encodeiCalLink(_user.id, _user.apiToken)}}" rel=tooltip title="Google Calendar"><i class=icon-calendar></i></a> -->
</span>
<div id="todos-chart" style="display:none;"></div>
<ul class="nav nav-tabs">
<li class="active"><a x-bind=click:changeContext data-target="#todo-well" data-context="context-uncompleted">Remaining</a></li>
<li><a x-bind=click:changeContext data-target="#todo-well" data-context="context-completed">Complete</a></li>
</ul>
<h2 class="task-column_title">Todos</h2>
</div>
<div class="span3 well rewards">
<!--Title -->
<div class="row-fluid">
<div class="span6"><h2>Rewards</h2></div>
<div class="span6">
{#unless _view.activeTabPets}
<table><tr>
<td>{gold(_user.stats.gp)} </td>
<td><div class='shop_gold' rel='tooltip' title='Gold'></div></td>
<td>{silver(_user.stats.gp)} </td>
<td><div class='shop_silver' rel='tooltip' title='Silver'></div></td>
<!--<td>{copper(_user.stats.gp)} </td>
<td><div class='shop_copper' rel='tooltip' title='shop_copper'></div></td>-->
</tr></table>
{else}
<app:userTokens />
{/}
</div>
</div>
<app:newTask type="reward" inputValue="{_newReward}" placeHolder="New Reward" />
{#if _user.flags.petsEnabled}
<div class='tabbable tabs-below'>
<div class="tab-content">
<div class="tab-pane active" id="rewards-tab">
<app:rewardsTab />
</div>
<div class="tab-pane" id="pets-tab">
{#with _view.items.pets as :pets}
<div class="row-fluid">
{#with :pets[0]}<app:pet />{/}
{#with :pets[1]}<app:pet />{/}
{#with :pets[2]}<app:pet />{/}
{#with :pets[3]}<app:pet />{/}
</div>
<div class="row-fluid">
{#with :pets[4]}<app:pet />{/}
{#with :pets[5]}<app:pet />{/}
{#with :pets[6]}<app:pet />{/}
{#with :pets[7]}<app:pet />{/}
</div>
<div class="row-fluid">
{#with :pets[8]}<app:pet />{/}
{#with :pets[9]}<app:pet />{/}
{#with :pets[10]}<app:pet />{/}
{#with :pets[11]}<app:pet />{/}
</div>
<div class="row-fluid">
{#with :pets[12]}<app:pet />{/}
{#with :pets[13]}<app:pet />{/}
{#with :pets[14]}<app:pet />{/}
{#with :pets[15]}<app:pet />{/}
</div>
<div class="row-fluid">
{#with :pets[16]}<app:pet />{/}
</div>
{/}
</div>
<!--<div class="tab-pane" id="mounts-tab">...</div>-->
<!-- create new todo -->
<div class="display-context-dependant show-for-uncompleted">
<app:tasks:newTask type="todo" inputValue="{_newTodo}" placeHolder="New Todo" />
</div>
<!-- list of all the todos -->
<ul class='todos task-list'>
{#each _todoList as :task}<app:tasks:task />{/}
</ul>
<ul class="nav nav-tabs" id="rewardsTabs">
<li class="active"><a href="#rewards-tab" data-toggle="tab" x-bind="click:activateRewardsTab">Rewards</a></li>
<li><a href="#pets-tab" id='pets-tab' data-toggle="tab" x-bind='click:activatePetsTab'>Pets</a></li>
<!--<li><a href="#mounts-tab" data-toggle="tab">Mounts</a></li>-->
<button class='task-action-btn tile spacious bright display-context-dependant show-for-completed' x-bind=click:clearCompleted>Clear Completed</button>
<div id="todos-chart" style="display:none;"></div>
<!-- remaining/completed tabs -->
<ul class="todo-status-toggler nav nav-tabs">
<li class="active"><a x-bind=click:changeContext data-target="#todo-well" data-context="context-uncompleted">Remaining</a></li>
<li><a x-bind=click:changeContext data-target="#todo-well" data-context="context-completed">Complete</a></li>
</ul>
</div>
{else}
<app:rewardsTab />
{/}
</div>
<!-- Rewards Column -->
<app:rewards:rewardsColumn />
<!-- Templates -->
<newTask:>
<form class="form-inline new-task-form" id="new-{@type}" data-task-type="{@type}" x-bind="submit:addTask">
<input value="{@inputValue}" type="text" name="new-task" placeholder="{@placeHolder}"/>
<input class="btn" type="submit" value="Add" />
<form class="addtask-form form-inline new-task-form" id="new-{@type}" data-task-type="{@type}" x-bind="submit:addTask">
<span class="addtask-field"><input value="{@inputValue}" type="text" name="new-task" placeholder="{@placeHolder}"/></span>
<input class="addtask-btn" type="submit" value="">
</form>
<hr>
<!-- all the parts of a single task -->
<task:>
<li data-id={{:task.id}} class="task {taskClasses(:task.type, :task.completed, :task.value, :task.repeat, :task.tags, _user._tagFilters)}">
<pre>
<div class="task-meta-controls">
<li data-id={{:task.id}} class="task {taskClasses(:task, _user._tagFilters)}">
<div class="hover-show">
<!-- right-hand side control buttons -->
<div class="task-meta-controls">
<!-- edit -->
<a x-bind=click:toggleTaskEdit data-hide-id="{{:task.id}}-chart" data-toggle-id="{{:task.id}}-edit" rel=tooltip title="Edit"><i class="icon-pencil"></i></a>
<!-- delete -->
<a x-bind=click:del rel=tooltip title="Delete"><i class="icon-trash"></i></a>
<!-- chart -->
<!-- removing for now cuz it's broken -->
{#if :task.history}
<a x-bind=click:toggleChart data-toggle-id="{{:task.id}}-chart" data-hide-id="{{:task.id}}-edit" data-history-path="_user.tasks.{{:task.id}}.history" rel="tooltip" title="Progress">
<i class="icon-signal"></i>
</a>
<!-- <a x-bind=click:toggleChart data-toggle-id="{{:task.id}}-chart" data-hide-id="{{:task.id}}-edit" data-history-path="_user.tasks.{{:task.id}}.history" rel="tooltip" title="Progress"><i class="icon-signal"></i></a> -->
{/}
<!-- notes -->
{#if :task.notes}
<span rel="popover" data-trigger="hover" data-placement="left" data-content="{:task.notes}" data-original-title="{:task.text}" class='task-notes'><i class="icon-comment"></i></span>
{/}
</div>
{#if :task.notes}
<span rel="popover" data-trigger="hover" data-placement="left" data-content="{:task.notes}" data-original-title="{:task.text}" class='task-notes'><i class="icon-comment"></i></span>
{/}
</div>
<!-- left-hand side checkbox -->
<div class="task-controls">
<!-- Habits -->
{#if equal(:task.type, 'habit')}
{#if :task.up}<a class="task-action-btn" data-direction=up x-bind=click:score></a>{/}
{#if :task.down}<a class="task-action-btn" data-direction=down x-bind=click:score></a>{/}
<div class="task-controls">
<!-- Habits -->
{#if equal(:task.type, 'habit')}
{#if :task.up}<a data-direction=up x-bind=click:score><img src="/img/add.png" /></a>{/}
{#if :task.down}<a data-direction=down x-bind=click:score><img src="/img/remove.png" /></a>{/}
<!-- Rewards -->
{else if equal(:task.type, 'reward')}
<a x-bind=click:score class="buy-link" data-direction=down>{:task.value}<img src="/img/coin_single_gold.png"/></a>
<!-- Daily & Todos -->
{else}
<input type=checkbox checked="{:task.completed}"/>
{/}
</div>
<div class="task-text">
{:task.text}
{#each _user.tags as :tag}
{#if :task.tags[:tag.id]}
<span class="label {#if _user._tagFilters[:tag.id]}label-info{/}">{:tag.name}</span>
<!-- Rewards -->
{else if equal(:task.type, 'reward')}
<a class="money btn-buy" x-bind=click:score data-direction=down>
<span class="reward-cost">{:task.value}</span>
<span class='shop_gold'></span>
</a>
<!-- Daily & Todos -->
{else}
<span class="task-checker action-yesno">
<input type=checkbox id="box-{{:task.id}}" class="visuallyhidden focusable" checked="{:task.completed}">
<label for="box-{{:task.id}}"></label>
</span>
{/}
{/}
</div>
</div>
<app:taskMeta />
</pre>
<!-- main content -->
<p class="task-text">
{:task.text}
{#each _user.tags as :tag}
{#if :task.tags[:tag.id]}
<span class="label {#if _user._tagFilters[:tag.id]}label-info{/}">{:tag.name}</span>
{/}
{/}
</p>
<!-- edit/options dialog -->
<app:tasks:taskMeta />
</li>
<!-- task edit/options -->
<taskMeta:>
<div style="display:none;" id={{:task.id}}-edit>
<hr/>
<div id="{{:task.id}}-edit" class="task-options visuallyhidden">
<form x-bind=submit:toggleTaskEdit data-toggle-id="{{:task.id}}-edit">
<div class=control-group>
<label>Text</label><input type=text value={:task.text} />
<label>Notes</label><textarea rows=3>{:task.notes}</textarea>
</div>
<!-- text & notes -->
<fieldset class="option-group">
<label class="option-title">Text</label><input class="option-content" type=text value={:task.text}>
<label class="option-title">Extra Notes</label><textarea class="option-content" rows=3>{:task.notes}</textarea>
</fieldset>
<!-- if Habit, plus/minus command options -->
{#if equal(:task.type, 'habit')}
<div class="control-group">
<label class="checkbox inline"><input type=checkbox checked={:task.up}>Up</label>
<label class="checkbox inline"><input type=checkbox checked={:task.down}>Down</label>
</div>
<fieldset class="option-group">
<legend class="option-title">Direction/Actions</legend>
<span class="task-checker action-plusminus select-toggle">
<input class="visuallyhidden focusable" type=checkbox id="{{:task.id}}-option-plus" checked={:task.up}>
<label for="{{:task.id}}-option-plus"></label>
</span>
<span class="task-checker action-plusminus select-toggle">
<input class="visuallyhidden focusable" type=checkbox id="{{:task.id}}-option-minus" checked={:task.down}>
<label for="{{:task.id}}-option-minus"></label>
</span>
</fieldset>
<!-- if Daily, calendar -->
{else if equal(:task.type, 'daily')}
<label>Repeat</label>
<div class="control-group btn-group repeat-days">
<!-- note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding -->
<button type="button" class="btn btn-info {#if :task.repeat.su}active{/}" data-day='su' x-bind=click:toggleDay>Su</button>
<button type="button" class="btn btn-info {#if :task.repeat.m}active{/}" data-day='m' x-bind=click:toggleDay>M</button>
<button type="button" class="btn btn-info {#if :task.repeat.t}active{/}" data-day='t' x-bind=click:toggleDay>T</button>
<button type="button" class="btn btn-info {#if :task.repeat.w}active{/}" data-day='w' x-bind=click:toggleDay>W</button>
<button type="button" class="btn btn-info {#if :task.repeat.th}active{/}" data-day='th' x-bind=click:toggleDay>Th</button>
<button type="button" class="btn btn-info {#if :task.repeat.f}active{/}" data-day='f' x-bind=click:toggleDay>F</button>
<button type="button" class="btn btn-info {#if :task.repeat.s}active{/}" data-day='s' x-bind=click:toggleDay>S</button>
</div>
<fieldset class="option-group">
<legend class="option-title">Repeat</legend>
<div class="task-controls tile-group repeat-days">
<!-- note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding -->
<button type="button" class="task-action-btn tile {#if :task.repeat.su}active{/}" data-day='su' x-bind=click:toggleDay>Su</button>
<button type="button" class="task-action-btn tile {#if :task.repeat.m}active{/}" data-day='m' x-bind=click:toggleDay>M</button>
<button type="button" class="task-action-btn tile {#if :task.repeat.t}active{/}" data-day='t' x-bind=click:toggleDay>T</button>
<button type="button" class="task-action-btn tile {#if :task.repeat.w}active{/}" data-day='w' x-bind=click:toggleDay>W</button>
<button type="button" class="task-action-btn tile {#if :task.repeat.th}active{/}" data-day='th' x-bind=click:toggleDay>Th</button>
<button type="button" class="task-action-btn tile {#if :task.repeat.f}active{/}" data-day='f' x-bind=click:toggleDay>F</button>
<button type="button" class="task-action-btn tile {#if :task.repeat.s}active{/}" data-day='s' x-bind=click:toggleDay>S</button>
</div>
</fieldset>
<!-- if Reward, pricing -->
{else if equal(:task.type, 'reward')}
<div class=control-group>
<label>Price
<div class=control-group>
<label>Price</label>
<div class="input-append">
<input class="span5" size="16" type="number" min="0" value={:task.value}><span class="add-on">Gold</span>
</div>
</label>
</div>
{else if equal(:task.type, 'todo')}
<div class=control-group>
<label>Due Date</label>
<input type="text" value="{:task.date}" data-date-format="mm/dd/yyyy" class="datepicker" />
<div><small>Enter as date, eg 02/19/2013 or 02-19-2013</small></div>
</div>
<!-- if Todo, the due date -->
{else if equal(:task.type, 'todo')}
<fieldset class="option-group">
<legend class="option-title">Due Date</legend>
<input class="option-content datepicker" type="text" value="{:task.date}" data-date-format="mm/dd/yyyy">
</fieldset>
{/}
{#if not(equal(:task.type, 'reward'))}
<!-- Advanced Options -->
{#unless equal(:task.type, 'reward')}
<p x-bind="click:tasksToggleAdvanced" class="option-title mega">Advanced Options</p>
<fieldset class="option-group advanced-option visuallyhidden">
<legend class="option-title"><a class='priority-multiplier-help' href="https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17" target="_blank"><i class='icon-question-sign'></i></a> Difficulty</legend>
<div class="task-controls tile-group priority-multiplier" data-id="{{:task.id}}">
<button type="button" class="task-action-btn tile {#if equal(:task.priority,'!')}active{/}{#unless :task.priority}active{/}" data-priority='!' x-bind=click:tasksSetPriority>Easy</button>
<button type="button" class="task-action-btn tile {#if equal(:task.priority,'!!')}active{/}" data-priority='!!' x-bind=click:tasksSetPriority>Medium</button>
<button type="button" class="task-action-btn tile {#if equal(:task.priority,'!!!')}active{/}" data-priority='!!!' x-bind=click:tasksSetPriority>Hard</button>
</div>
</fieldset>
{/}
<div class="control-group">
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
@ -227,8 +204,8 @@
</ul>
</div>
</div>
{/}
<button type=submit class="btn" x-bind="click:tasksSaveAndClose">Save & Close</button>
<button type=submit class="task-action-btn tile spacious" x-bind="click:tasksSaveAndClose">Save & Close</button>
</form>
</div>

View file

@ -1,337 +0,0 @@
<Title:>
Privacy Policy
<Body:>
<div class="well">
<a href="/">&laquo; Back</a>
<div class="page-header">
<h1>Privacy Policy</h1>
</div>
<p class="pagemeta">
Last updated September 02, 2012
</p>
<p>
<strong>PLEASE READ THIS PRIVACY POLICY CAREFULLY.</strong>
<br>
By accessing
or otherwise using habitrpg.com or any sub domains thereto ("the Sites"),
or using a habitrpg.com or HabitRPG application on a mobile device ("the Applications"),
you agree to be bound contractually by this Privacy Policy. Individually
or collectively, the Applications and the Sites may be referred to as
the "Services."
</p>
<p>
To review material modifications and their effective dates scroll
to the bottom of the page.
</p>
<p>
<strong> 1. Privacy Statement; Collection of Personal
Information. </strong>
<br>
1.1 OCDevel owns and operates this business. All
references to "we", "us", shall be construed to mean OCDevel.
</p>
<p>
1.2 We understand that visitors to this website are concerned
about the privacy of information. The following describes our privacy
policy regarding information, including personal information, that we
collect through this website.
</p>
<p>
<strong>2. Modification of Privacy Policy.</strong>
<br>
We reserve the right to modify this Privacy Policy at any time,
and without prior notice, by posting an amended Privacy Policy that is
always accessible by clicking on the "Privacy Policy" link on this
site's home page. Your continued use of this site indicates your
acceptance of the amended Privacy Policy. You should check the Privacy
Policy through this link periodically for modifications by clicking on
the link provided near the top of the Privacy Policy for a listing of
material modifications and their effective dates. Regarding personal
information, if any modifications are materially less restrictive on our
use or disclosure of the personal information previously disclosed by
you, we will obtain your consent before implementing such revisions with
respect to such information.
</p>
<p>
<strong>3. Collection of Anonymous, Passive Information.</strong>
<br>
We reserve the right to monitor your use of the services. As you
navigate through the services, certain anonymous information may be
passively collected (that is, gathered without your actively providing
the information) using various technologies, such as cookies, Internet
tags or web beacons, and navigational data collection (log files, server
logs, clickstream). The following is a listing and a brief explanation
of passive information collection methodologies which we may use from
time to time to better understand how the Services are being used.
</p>
<p>
3.1 A "cookie" is a text file that this site sends to your
browser in the form of a text file The information generated by the
cookie about your use of this site (including your IP address) will be
transmitted to and stored. Most browsers automatically accept cookies,
but they usually can be modified to decline cookies if you prefer;
however, certain features of this site might not work without cookies.
</p>
<p>
3.2 "Session" cookies are temporary bits of information that are
used to improve navigation, block visitors from providing information
where inappropriate (the Services "remembers" previous entries of age or
country of origin that were outside the specified parameters and blocks
subsequent changes), and collect aggregate statistical information on
the Services. They are erased once you exit your Web browser or
otherwise turn off your computer.
</p>
<p>
3.3 "Persistent" cookies are more permanent bits of information
that are placed on the hard drive of your computer and stay there unless
you delete the cookie. Persistent cookies store information on your
computer for a number of purposes, such as retrieving certain
information you have previously provided, helping to determine what
areas of the Services you may find most valuable, and customizing the
Services based on your preferences on an ongoing basis. Persistent
cookies placed by this site in your computer do not hold personal
information.
</p>
<p>
3.4 You can set your browser to accept all cookies, to reject all
cookies, or to notify you whenever a cookie is offered so that you can
decide each time whether to accept it. To learn more about cookies and
how to specify your preferences, please search for "cookie" in the
"Help" portion of your browser.
</p>
<p>
3.5 An Internet Protocol (IP) address is a number assigned to
your computer by your Internet service provider so you can access the
Internet and is generally considered to be non-personally identifiable
information, because in most cases an IP address is dynamic (changing
each time you connect to the Internet), rather than static (unique to a
particular user's computer). The IP address can be used to diagnose
problems with a server, report aggregate information, determine the
fastest route for your computer to use in connecting to a site, and
administer and improve the Services.
</p>
<p>
3.6 "Internet tags" (also known as Web Beacons, single-pixel
GIFs, clear GIFs, invisible GIFs, and 1-by-1 GIFs) are smaller than
cookies and tell the Web site server information such as the IP address
and browser type related to the visitor's computer. Tags may be placed
both on online advertisements that bring people to the Services and on
different pages of the Services. Such tags indicate how many times a
page is opened and which information is consulted.
</p>
<p>
3.7 "Navigational data" (log files, server logs, and clickstream
data) are used for system management, to improve the content of the
Services, market research purposes, and to communicate information to
visitors.
</p>
<p>
<strong>4. Use and Sharing of Anonymous, Passive Information.</strong>
<br>
The Services may make full use of passively collected anonymous
information, including without limitation the right to use such
information to provide better service to Service users, customize the
Services based on your preferences, compile and analyze statistics and
trends, and otherwise administer and improve the Services for your use. We
reserve the right to share this anonymous, passive information in
aggregated form.
</p>
<p>
<strong>5. 3rd Party Behavioral Ads; Google's AdSense Network.</strong>
<br>
5.1 We reserve the right to use anonymous, passive information
about your visits to this and other websites (not including your name,
address, email address or telephone number) for purposes of serving our
ads and third party ads that are targeted to your interests ("3rd Party
Behavioral Ads"). We reserve the right to share anonymous, passive
information collected on the services with third parties for purposes of
serving 3rd Party Behavioral Ads. These 3rd Party Behavioral Ads do not
identify you personally. Instead, they associate your behavioral data on
visited sites with your browser, so that the ads your computer sees on
this site are more likely to be relevant to your interests. 3rd Party
Behavioral Ads require that that you be served with a cookie containing
a tracking code. You may refuse the use of cookies by selecting the
appropriate settings on your browser; however, please note that if you
do this you may not be able to use the full functionality of this site.
</p>
<p>
5.2 We reserve the right to participate in Google's AdSense
network for purposes of serving 3rd Party Behavioral Ads. Google uses
DoubleClick's DART cookie for serving 3rd Party Behavioral Ads over the
AdSense network. You may opt out of the use of the DART cookie. For
information regarding how to opt out, go to
http://www.google.com/privacy_ads.html.
</p>
<p>
<strong>6. Use of 3rd Party Analytics.</strong>
<br>
We reserve the right to use analytics services provided by
third parties. These services use 3rd party cookies to collect
anonymous, passive information about your use of this site (see
explanation of cookies in Collection of Anonymous, Passive Information
above). We use this information for the purpose of evaluating your use
of the Services, compiling reports on activity, and providing other
services. These web analytics services may also transfer this
information to third parties where required to do so by law, or where
such third parties process the information on the service's behalf.
</p>
<p>
<strong>7. Collection of Personal Information; Categories.</strong>
<br>
We will ask you for personal information when you sign up for any
specific benefit or purpose that requires registration. Personal
information that we collect may vary with the each registration, and it
may include one or more of the following categories: name, physical
address, an email address, phone number, and credit card information
including credit card number, expiration date, and billing address,
emergency contact information, current medications, allergies, medical
insurance information.
</p>
<p>
<strong> 8. Use And Sharing of Personal Information: General
Policy And Exceptions. </strong>
<br>
Our general policy is that we will use your personal information,
including combining your personal information with passive information
collected from this site, only for: the performance of the services or
transaction for which it was given, our private, internal reporting for
this site, and security assessments for this site, and we will not
share, sell, or rent your personal information to others. The only
exceptions to this general policy: (i) are described in the subsections
below, and (ii) if you explicitly approve through our site.
</p>
<p>
8.1 Affiliates And Service Providers. We reserve the right to
provide such information to our affiliates or subsidiaries, or trusted
service providers for the purpose of hosting our servers or processing
or archiving personal information for us. We require that these parties
agree to privacy and security safeguards for this information that are
consistent with this Privacy Policy.
</p>
<p>
8.2 Acquisition; Bankruptcy. In the event that we are acquired by
or merged with a third party entity, we reserve the right to transfer
such information as part of such merger, acquisition, sale, or other
change of control. In the unlikely event of our bankruptcy, insolvency,
reorganization, receivership, or assignment for the benefit of
creditors, or the application of laws or equitable principles affecting
creditors' rights generally, we reserve the right to transfer such
information to protect our rights or as required by law.
</p>
<p>
8.3 Enforcement; Legal Process. We reserve the right to transfer
such information if we have a good faith belief that access, use,
preservation or disclosure of such information is reasonably necessary
(i) to satisfy any applicable law, regulation, legal process or
enforceable governmental request, or (ii) to investigate or enforce
violations of our rights or the security of this site.
</p>
<p>
8.4 Miscellaneous. We reserve the right to share personal
information with the following additional parties: online organizers
using our tools and resellers of our products and services from whose
site the sale originated (even though the sale originates at site of the
reseller, registration and collection of personal information occurs at
this site).
</p>
<p>
<strong> 9. Onward Transfer of Personal Information Outside Your
Country of Residence. </strong>
<br>
Any personal information which we may collect on this site will
be stored and processed in our servers located only in the United
States. By using this site, if you reside outside the United States, you
consent to the transfer of personal information outside your country of
residence to the United States.
</p>
<p>
<strong>10. Security of Personal Information.</strong>
<br>
We follow reasonable and appropriate industry standards to
protect your personal information and data. Unfortunately, no data
transmission over the Internet or method of data storage can be
guaranteed 100% secure. Therefore, while we strive to protect your
personal information by following generally accepted industry standards,
we cannot ensure or warrant the absolute security of any information you
transmit to us or archive at this site.
</p>
<p>
<strong>11. Changing And Updating Personal Information.</strong>
<br>
Upon request, we will permit you to request or make changes or
updates to your personal information for legitimate purposes. We request
identification prior to approving such requests. We reserve the right to
decline any requests that are unreasonably repetitive or systematic,
require unreasonable time or effort of our technical or administrative
personnel, or undermine the privacy rights of others. We reserve the
right to permit you to access your personal information in any account
you establish with this site for purposes of making your own changes or
updates, and in such case, instructions for making such changes or
updates will be provided where necessary.
</p>
<p>
<strong>12. Email From This Site; Opt-Out Rights.</strong>
<br>
If you supply us with your e-mail address you may receive
periodic messages from us with information specific to the Services and
required for the normal functioning of the Services as well as for new
products or services or upcoming events. If you prefer not to receive
periodic email messages, you may opt-out by following the instructions
on the email.
</p>
<p>
<strong>13. Children's Online Policy.</strong>
<br>
We are committed to preserving online privacy for all of its
website visitors, including children. This site is a general audience
site. Consistent with the Children's Online Privacy Protection Act
(COPPA), we will not knowingly collect any information from, or sell to,
children under the age of 13. If you are a parent or guardian who has
discovered that your child under the age of 13 has submitted his or her
personally identifiable information without your permission or consent,
we will remove the information from our active list, at your request. To
request the removal of your child's information, please send contact our
site as provided below under “Contact Us”, and be sure to include in
your message the same login information that your child submitted.
</p>
<p>
<strong> 14. Email And Other Messages Through This Site; ECPA
Notice. </strong>
<br>
This site treats email messages and other electronic messages
that are sent through this site and not viewable by others as
confidential and private, except as required by law, including without
limitation, the Electronic Communications Privacy Act of 1986, 18 U.S.C.
Sections 2701-2711 (the "ECPA"). The ECPA permits this site's limited
ability to intercept and/or disclose electronic messages, for example
(i) as necessary to operate our system or to protect our rights or
property, (ii) upon legal demand (court orders, warrants, subpoenas), or
(iii) where we receive information inadvertently which appears to
pertain to the commission of a crime. This site is not considered a
"secure communications medium" under the ECPA.
</p>
<p>
<strong>15. Contact Us.</strong>
<br>
If you have any questions regarding this Privacy Policy, please
contact the owner and operator of this website business:
</p>
<address>
<strong>OCDevel</strong>
<br>
98 Morrison Ave #2
<br>
Somerville, MA 02144
<br>
Email:
<a href="mailto:tylerrenelle@gmail.com">tylerrenelle@gmail.com</a>
<br>
Telephone: 1-805-975-5236
</address>
<p></p>
<div id="home_static_footer">
© 2012 OCDevel
</div>
</div>

67
views/static/about.html Normal file
View file

@ -0,0 +1,67 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | About
<Head:>
<app:head:head active="about"/>
<Body:>
<div id="wrap">
<app:header:header />
<div class='container'>
<section>
<div class="page-header">
<h1>About</h1>
</div>
<p class="lead">HabitRPG is an <a href="https://github.com/lefnire/habitrpg">open source</a> habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.</p>
</section>
<div class='marketing'>
<p><iframe src="http://player.vimeo.com/video/57654086" width="960" height="539" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe></p>
</div>
<section>
<div class="page-header">
<h2>Habits</h2>
</div>
<p class="lead">Habits are situational goals that you constantly track. You can either try to break bad habits or reinforce good ones. You may encounter some habits multiple times a day (like "Floss After Eating"), and some you may not encounter often at all (like "Replace the Toilet Paper When it Runs Out"). For some habits, it only makes sense to gain points (like "Do One Hour of Productive Work"). For others, it only makes sense to lose points (like "Eat Junk Food"). For the rest, both gain and loss apply (like for "Take The Stairs", taking the stairs would be a gain while taking the elevator would be a loss).</p>
</section>
<section>
<div class="page-header">
<h2>Dailies</h2>
</div>
<p class="lead">Dailies are goals that you want to complete once a day, building them into your routine (like "Workout for 30 Minutes"). Unlike Habits, Dailies may only be checked off once a day. Also unlike habits, at the end of each day non-completed Dailies will cause damage, making you lose Hit Points. If you are doing well and consistantly check off a Daily day after day, it will turn green and earn less gold and experience, though it will also cause you to lose fewer Hit Points if you skip it. This means you can ease up on it for a bit. Conversely, if you are doing poorly and fail to check something off every day, it will turn red. The worse you do, the more experience and gold that Daily is worth and the more hit points it takes away if left uncompleted. This encourages you to focus on your shortcomings, the reds. Oh, and don't worry. You can make a Daily active only for certain days of the week, so weekday tasks won't pester you on the weekends. Or you can make a Daily active only on Mondays, thus creating a weekly task.</p>
</section>
<section>
<div class="page-header">
<h2>Todos</h2>
</div>
<p class="lead">Todos are one-time goals which need to be completed eventually (like "Wash the car" or "Buy milk"). Non-completed Todos wont hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos.</p>
</section>
<section>
<div class="page-header">
<h2>Rewards</h2>
</div>
<p class='lead'>As you complete goals, you earn gold to buy rewards that you create (like "An Hour of Video Games"). Create plenty of rewards and buy them liberally! Rewarding good performance is the best way to reinforce good habits. If you really need a reward but don't have enough gold, you can still make a purchase. But be careful because it will cost you Hit Points!</p>
<p class='lead'>After youve reached level 2, you unlock the <strong>Item Store</strong> under the rewards column. You can now buy weapons, armor, and potions. Armor keeps you protected by reducing how much damage you take from failed tasks. Weapons increase how much experience you gain for completing a task. Potions recover 15 HP, for when you've piled just a little too much onto your schedule.</p>
</section>
<section>
<div class="page-header">
<h2>License & Credits</h2>
</div>
<p class='lead'>Code is licensed under GNU GPL v3. Content is licensed under CC-BY-SA 3.0. See the LICENSE file for details.</p>
<p class='lead'>Content and assets comes from Mozillas <a href="http://browserquest.mozilla.org/">BrowserQuest</a> (<a href="http://mozilla.org">Mozilla</a>, <a href="http://www.littleworkshop.fr">Little Workshop</a>). They seriously rock.
</p>
</section>
</div>
</div>
<app:footer:footer />

View file

@ -0,0 +1,43 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | Add-ons & Extensions
<Head:>
<app:head:head active="about"/>
<Body:>
<div id="wrap">
<app:header:header />
<div class='container'>
<section>
<div class="page-header">
<h1>Add-ons & Extensions</h1>
</div>
</section>
<h2 id="chrome-extension"><a href="https://chrome.google.com/webstore/detail/habitrpg/pidkmpibnnnhneohdgjclfdjpijggmjj">Chrome Extension</a></h2>
<p>Monitors your browsing habits to divvy points to your HabitRPG character. Think Stayfocusd for HabitRPG (http://habitrpg.com).</p>
<p>Lose HP for lingering on bad websites, gain Exp and GP for hanging out on good websites. Set the hours and days of the week you want to be productive and integrate your browsing habits with HabitRPG to improve your overall productivity.</p>
<p>To use, enter your HabitRPG User ID and API Token in the options page (User -&gt; Settings, see the screenshot). Hit me up with suggestions</p>
<h2 id="pomodoro">Pomodoro</h2>
<p>I use Ugo Landini's Pomodoro, which allows start / stop custom scripts: <code>shell do shell script &quot;curl -X POST -H 'Content-Type:application/json' https://habitrpg.com/v1/users/.../tasks/productivity/up -d '{\\&quot;apiToken\\&quot;:\\&quot;...\\&quot;}&quot;</code></p>
<h2 id="anki"><a href="https://github.com/Pherr/HabitRPG-Anki-Addon">Anki</a></h2>
<p>A HabitRPG addon for Anki.</p>
<h2 id="habitrpg-mobile"><a href="https://github.com/HabitRPG/habitrpg-mobile">HabitRPG Mobile</a></h2>
<p>HabitRPG mobile application under development. Angular + PhoneGap</p>
<h2 id="habitrpg-ncurses"><a href="https://github.com/arscan/habitrpg-ncurses">habitrpg-ncurses</a></h2>
<p>node + ncurses interface for habitRPG.</p>
<p>This is set up to meet my general task workflow and my preferences. Todo lists are simple enought that I feel like investing time into making something that works exactly how I want it to work is worthwhile. Plus I felt like learning ncurses. At this point, attempt to use at your own risk.</p>
<p>Apparently the ncurses binding doesn't seem to work properly when loaded from npm. Grabbing the master branch from https://github.com/mscdex/node-ncurses works for me.</p>
<h2 id="pyhabit"><a href="https://github.com/elssar/pyhabit">pyhabit</a></h2>
<p>Python command line app for HabitRPG (http://habitrpg.com)</p>
<h2 id="php-api"><a href="http://github.com/ruddfawcett/HabitRPG_PHP">PHP API</a></h2>
<p>A PHP class for the HabitRPG API. Will be expanded when more features become available in the API.</p>
<h2 id="suggest-an-add-on-extension">Suggest an Add-on / Extension</h2>
<p><a href="mailto:tylerrenelle@gmail.com">Email me</a> to have something listed here</p>
</div>
</div>
<app:footer:footer />

107
views/static/faq.html Normal file
View file

@ -0,0 +1,107 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | FAQ
<Head:>
<app:head:head active="about"/>
<Body:>
<div id="wrap">
<app:header:header />
<div class='container'>
<section>
<div class="page-header">
<h1>FAQ</h1>
</div>
</section>
<section id="habit-tracking">
<div class="page-header">
<h2>What is habit tracking?</h2>
</div>
<p>Habit tracking is keeping tabs on all your habits to help you improve. It is to life what calorie-counting is to diets (and calorie-counting falls under the habit-tracking umbrella). See <a href="http://blog.beeminder.com/trackhack/">Trackers vs Lifehackers</a> for more information and website examples.</p>
<p>There are many habit-tracking apps, but HabitRPG takes a different spin: treating your goals and habits like a Role Playing Game (RPG). If you've played RPGs growing up, this app will make sense to you; otherwise, it may be overwhelming at first. Be sure to follow the tutorial when you first load the website, and if you're still confused watch this <a href="/static/about">video tutorial</a></p>
</section>
<section id="which-task-type">
<div class="page-header">
<h2>Should this task be a Habit, Daily, or Todo?</h2>
</div>
<p>See <a href="https://habitrpg.com/static/about">About</a> on task type definitions, but think of it like this.</p>
<ul>
<li>Habits you track many times per day. Taking the stairs, eating candy, biting your nails, etc. They're sort of micro-tasks. They're also the dumping ground for 3rd-party tasks, created via API calls - for example, when you visit vice websites (Reddit, 9gag) while using the <a href="https://chrome.google.com/webstore/detail/habitrpg/pidkmpibnnnhneohdgjclfdjpijggmjj">Chrome extension</a> - that's updated as a habit.</li>
<li>Dailies are things you track once or twice a day. Exercising, 1h Reading, Anki Session, etc. They reset at the end of each day, and must be completed each day. The confusion is usually between Dailies and Habits, and it usually goes like this: &quot;Should I have a Daily called <em>1h Reading</em> (I want to read 1 hour each day), or a Habit called <em>1h Reading</em> that I up-score for every hour I read in a day?&quot;. My answer - Daily. If it's a decently difficult task that you want to become a good habit, make it a Daily - you're more likely to do it when it loses points at the end of each day than if it sits.</li>
<li>Todos are things you'll do once and only once. <em>Call Mom</em>, <em>Send eBay Package</em>, <em>Pay Doctor Bill</em>, etc. Pretty straight forward.</li>
</ul>
</section>
<section id="what-are-tokens">
<div class="page-header">
<h2>What are tokens?</h2>
</div>
<p>In the days of yore, arcade machines ran on tokens or quarters, and were known to gobble such nuggets with avaricious glee. HabitRPG has no wish to drain your pocket change, but those who use and appreciate it can support the project by turning real-world quarters into tokens, and using the tokens in turn to reroll your tasks, spruce up their character, and tame fearsome pets & mounts to impress friend and foe alike!</p>
</section>
<section id="how-do-i-reset">
<div class="page-header">
<h2>How do I reset?</h2>
</div>
<p>Many people have requested the ability to start completely from scratch. This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while - once they've become accustomed to how things work, they want to start playing &quot;for real&quot;. To do this, make sure you're logged in and click the profile drop-down in the top-right corner, then click &quot;Reset&quot;.</p>
</section>
<section id="how-do-i-customize-a-tasks-value">
<div class="page-header">
<h2>How do I customize a task's value?</h2>
</div>
<p>Some tasks are more valuable to you than others. Finishing your research paper, for example, should be worth more points than flossing your teeth. Instead of customizing value upon task-creation, HabitRPG calculates a task's value automatically over time. The longer something sits without being completed (Dailies and Todos), the more valuable it becomes; and the more frequently you complete Dailies, the less valuable they become. Trust in the system's machine-learning of task-values.</p>
<p>If you <em>absolutely</em> must customize a task's value, there's a &quot;difficulty&quot; setting under the Advanced section of task-editing.</p>
</section>
<section id="vacation">
<div class="page-header">
<h2>I'm going on vacation, how can I disable my Dailies?</h2>
</div>
<p>Dailies have a &quot;Repeat&quot; field, which specifies which week days this item is due. When you on go vacation, just set each Daily's repeats such that they aren't due on any week days at all. <a href="https://www.evernote.com/shard/s17/sh/e362a4ec-97aa-4c35-8a09-74dc5c16a0fe/a9e895c1f93f4398c56eedef0e58c6fe/res/a44859e5-726e-4e1f-817a-d4f37adbc6f9/skitch.png?resizeSmall&amp;width=832">See this screenshot</a>. As for Todos, they will still gain value but they won't hurt you.</p>
</section>
<section id="beta-access">
<div class="page-header">
<h2>Beta Access</h2>
</div>
<h3>Release Date</h3>
<p>The mobile apps are slated to be released in April / May 2013. We hope sooner than that, but that's our current estimate. For the Kickstarter beta access, here's how we'll handle that:</p>
<h3>Website</h3>
<p>If you like to live dangerously, you can use <a href="https://beta.habitrpg.com">beta.habitrpg.com</a> instead of the production site, which will introduce new features faster. It tracks the production database, so yes your account will be synced.</p>
<h3>Mobile App</h3>
<p>We'll post updates on the Kickstarter when we have a beta version of the mobile app that y'all can play with (downloadable files, outside the various app stores), and will post each new beta version update on the KS update section as well.</p>
</section>
<section id="browserquest-art">
<div class="page-header">
<h2>BrowserQuest Art</h2>
</div>
<p>We're currently using the pixel art from <a href="https://github.com/mozilla/BrowserQuest">Mozilla BrowserQuest</a>. Some have been opposed because they've played BQ themselves, and don't like design recycling, but here's my reasoning:</p>
<p>It is licensed under CC-BY-SA 3.0 (so we can use it). Fortunately for us (but sadly for Mozilla), BQ is mothballed. That has two implications. First, only those in-the-know will recognize it - which is mostly developers &amp; designers who have been keeping abreast HTML5's progress. Second, it's absolutely superb in design and nostalgia - it does an excellent job of bringing back SNES, and it would be a shame to see it go to waste. Also, it's free, and will get pretty far before we need to start customizing.</p>
</section>
<section id="browser-support">
<div class="page-header">
<h2>Browser Support</h2>
</div>
<p>Everything except Internet Explorer. Derby doesn't currently support IE (see <a href="https://github.com/codeparty/derby/issues/126">this thread</a>), though support is <a href="https://github.com/codeparty/saddle">eventually planned</a>. Use Chrome for best results.</p>
</section>
<section id="how-can-i-get-involved">
<div class="page-header">
<h2>How can I get involved?</h2>
</div>
<p>Check out <a href="https://github.com/lefnire/habitrpg/wiki/Business-Model#count-me-in">"Count Me In!"</a>, we need all the help we can get!</p>
</section>
</div>
</div>
<app:footer:footer />

28
views/static/front.html Normal file
View file

@ -0,0 +1,28 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | Gamify Your Life
<Head:>
<app:head:head />
<Body:>
<div id="wrap">
<app:header:header active="front"/>
<div class='jumbotron masthead'>
<div class='container'>
<h1><img src="/img/logo/habitrpg_pixel.png" alt="HabitRPG"/></h1>
<p>A habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.</p>
<a href="/?play=1" class='btn btn-primary btn-small'>Play</a>
</div>
</div>
<div class='container'>
<p style="height:600;"><iframe src="http://player.vimeo.com/video/57639356" width="100%" height="539" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe></p>
</div>
</div>
<app:footer:footer />

17
views/static/head.html Normal file
View file

@ -0,0 +1,17 @@
<head:>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CDN -->
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap.min.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap-responsive.min.css" rel="stylesheet">
<link href="/vendor/bootstrap/docs/assets/css/docs.css" rel="stylesheet">
<link href="/css/static-pages.css" rel="stylesheet">
<link href="/css/footer.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
<script type="text/javascript">
$.getScript("//s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire");
</script>

33
views/static/header.html Normal file
View file

@ -0,0 +1,33 @@
<header:>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand {{#if equal(@active,'front')}}active{{/}}" href="/static/front">HabitRPG</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li class="{{#if equal(@active,'about')}}active{{/}}">
<a href="/static/about">About</a>
</li>
<li class="">
<a target="_blank" href="http://habitrpg.tumblr.com/">Blog</a>
</li>
<li class="{{#if equal(@active,'team')}}team{{/}}">
<a href="/static/team">Team</a>
</li>
<li class="{{#if equal(@active,'extensions')}}active{{/}}">
<a href="/static/extensions">Extensions</a>
</li>
<li class="{{#if equal(@active,'faq')}}active{{/}}">
<a href="/static/faq">FAQ</a>
</li>
</ul>
</div>
</div>
</div>
</div>

347
views/static/privacy.html Normal file
View file

@ -0,0 +1,347 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | Privacy
<Head:>
<app:head:head active="about"/>
<Body:>
<div id="wrap">
<app:header:header />
<div class='container'>
<div class="page-header">
<h1>Privacy Policy</h1>
</div>
<p class="pagemeta">
Last updated September 02, 2012
</p>
<p>
<strong>PLEASE READ THIS PRIVACY POLICY CAREFULLY.</strong>
<br>
By accessing or otherwise using habitrpg.com or any sub domains thereto ("the Sites"),
or using a habitrpg.com or HabitRPG application on a mobile device ("the Applications"),
you agree to be bound contractually by this Privacy Policy. Individually
or collectively, the Applications and the Sites may be referred to as
the "Services."
</p>
<p>
To review material modifications and their effective dates scroll
to the bottom of the page.
</p>
<p>
<strong> 1. Privacy Statement; Collection of Personal
Information. </strong>
<br>
1.1 OCDevel owns and operates this business. All
references to "we", "us", shall be construed to mean OCDevel.
</p>
<p>
1.2 We understand that visitors to this website are concerned
about the privacy of information. The following describes our privacy
policy regarding information, including personal information, that we
collect through this website.
</p>
<p>
<strong>2. Modification of Privacy Policy.</strong>
<br>
We reserve the right to modify this Privacy Policy at any time,
and without prior notice, by posting an amended Privacy Policy that is
always accessible by clicking on the "Privacy Policy" link on this
site's home page. Your continued use of this site indicates your
acceptance of the amended Privacy Policy. You should check the Privacy
Policy through this link periodically for modifications by clicking on
the link provided near the top of the Privacy Policy for a listing of
material modifications and their effective dates. Regarding personal
information, if any modifications are materially less restrictive on our
use or disclosure of the personal information previously disclosed by
you, we will obtain your consent before implementing such revisions with
respect to such information.
</p>
<p>
<strong>3. Collection of Anonymous, Passive Information.</strong>
<br>
We reserve the right to monitor your use of the services. As you
navigate through the services, certain anonymous information may be
passively collected (that is, gathered without your actively providing
the information) using various technologies, such as cookies, Internet
tags or web beacons, and navigational data collection (log files, server
logs, clickstream). The following is a listing and a brief explanation
of passive information collection methodologies which we may use from
time to time to better understand how the Services are being used.
</p>
<p>
3.1 A "cookie" is a text file that this site sends to your
browser in the form of a text file The information generated by the
cookie about your use of this site (including your IP address) will be
transmitted to and stored. Most browsers automatically accept cookies,
but they usually can be modified to decline cookies if you prefer;
however, certain features of this site might not work without cookies.
</p>
<p>
3.2 "Session" cookies are temporary bits of information that are
used to improve navigation, block visitors from providing information
where inappropriate (the Services "remembers" previous entries of age or
country of origin that were outside the specified parameters and blocks
subsequent changes), and collect aggregate statistical information on
the Services. They are erased once you exit your Web browser or
otherwise turn off your computer.
</p>
<p>
3.3 "Persistent" cookies are more permanent bits of information
that are placed on the hard drive of your computer and stay there unless
you delete the cookie. Persistent cookies store information on your
computer for a number of purposes, such as retrieving certain
information you have previously provided, helping to determine what
areas of the Services you may find most valuable, and customizing the
Services based on your preferences on an ongoing basis. Persistent
cookies placed by this site in your computer do not hold personal
information.
</p>
<p>
3.4 You can set your browser to accept all cookies, to reject all
cookies, or to notify you whenever a cookie is offered so that you can
decide each time whether to accept it. To learn more about cookies and
how to specify your preferences, please search for "cookie" in the
"Help" portion of your browser.
</p>
<p>
3.5 An Internet Protocol (IP) address is a number assigned to
your computer by your Internet service provider so you can access the
Internet and is generally considered to be non-personally identifiable
information, because in most cases an IP address is dynamic (changing
each time you connect to the Internet), rather than static (unique to a
particular user's computer). The IP address can be used to diagnose
problems with a server, report aggregate information, determine the
fastest route for your computer to use in connecting to a site, and
administer and improve the Services.
</p>
<p>
3.6 "Internet tags" (also known as Web Beacons, single-pixel
GIFs, clear GIFs, invisible GIFs, and 1-by-1 GIFs) are smaller than
cookies and tell the Web site server information such as the IP address
and browser type related to the visitor's computer. Tags may be placed
both on online advertisements that bring people to the Services and on
different pages of the Services. Such tags indicate how many times a
page is opened and which information is consulted.
</p>
<p>
3.7 "Navigational data" (log files, server logs, and clickstream
data) are used for system management, to improve the content of the
Services, market research purposes, and to communicate information to
visitors.
</p>
<p>
<strong>4. Use and Sharing of Anonymous, Passive Information.</strong>
<br>
The Services may make full use of passively collected anonymous
information, including without limitation the right to use such
information to provide better service to Service users, customize the
Services based on your preferences, compile and analyze statistics and
trends, and otherwise administer and improve the Services for your use. We
reserve the right to share this anonymous, passive information in
aggregated form.
</p>
<p>
<strong>5. 3rd Party Behavioral Ads; Google's AdSense Network.</strong>
<br>
5.1 We reserve the right to use anonymous, passive information
about your visits to this and other websites (not including your name,
address, email address or telephone number) for purposes of serving our
ads and third party ads that are targeted to your interests ("3rd Party
Behavioral Ads"). We reserve the right to share anonymous, passive
information collected on the services with third parties for purposes of
serving 3rd Party Behavioral Ads. These 3rd Party Behavioral Ads do not
identify you personally. Instead, they associate your behavioral data on
visited sites with your browser, so that the ads your computer sees on
this site are more likely to be relevant to your interests. 3rd Party
Behavioral Ads require that that you be served with a cookie containing
a tracking code. You may refuse the use of cookies by selecting the
appropriate settings on your browser; however, please note that if you
do this you may not be able to use the full functionality of this site.
</p>
<p>
5.2 We reserve the right to participate in Google's AdSense
network for purposes of serving 3rd Party Behavioral Ads. Google uses
DoubleClick's DART cookie for serving 3rd Party Behavioral Ads over the
AdSense network. You may opt out of the use of the DART cookie. For
information regarding how to opt out, go to
http://www.google.com/privacy_ads.html.
</p>
<p>
<strong>6. Use of 3rd Party Analytics.</strong>
<br>
We reserve the right to use analytics services provided by
third parties. These services use 3rd party cookies to collect
anonymous, passive information about your use of this site (see
explanation of cookies in Collection of Anonymous, Passive Information
above). We use this information for the purpose of evaluating your use
of the Services, compiling reports on activity, and providing other
services. These web analytics services may also transfer this
information to third parties where required to do so by law, or where
such third parties process the information on the service's behalf.
</p>
<p>
<strong>7. Collection of Personal Information; Categories.</strong>
<br>
We will ask you for personal information when you sign up for any
specific benefit or purpose that requires registration. Personal
information that we collect may vary with the each registration, and it
may include one or more of the following categories: name, physical
address, an email address, phone number, and credit card information
including credit card number, expiration date, and billing address,
emergency contact information, current medications, allergies, medical
insurance information.
</p>
<p>
<strong> 8. Use And Sharing of Personal Information: General
Policy And Exceptions. </strong>
<br>
Our general policy is that we will use your personal information,
including combining your personal information with passive information
collected from this site, only for: the performance of the services or
transaction for which it was given, our private, internal reporting for
this site, and security assessments for this site, and we will not
share, sell, or rent your personal information to others. The only
exceptions to this general policy: (i) are described in the subsections
below, and (ii) if you explicitly approve through our site.
</p>
<p>
8.1 Affiliates And Service Providers. We reserve the right to
provide such information to our affiliates or subsidiaries, or trusted
service providers for the purpose of hosting our servers or processing
or archiving personal information for us. We require that these parties
agree to privacy and security safeguards for this information that are
consistent with this Privacy Policy.
</p>
<p>
8.2 Acquisition; Bankruptcy. In the event that we are acquired by
or merged with a third party entity, we reserve the right to transfer
such information as part of such merger, acquisition, sale, or other
change of control. In the unlikely event of our bankruptcy, insolvency,
reorganization, receivership, or assignment for the benefit of
creditors, or the application of laws or equitable principles affecting
creditors' rights generally, we reserve the right to transfer such
information to protect our rights or as required by law.
</p>
<p>
8.3 Enforcement; Legal Process. We reserve the right to transfer
such information if we have a good faith belief that access, use,
preservation or disclosure of such information is reasonably necessary
(i) to satisfy any applicable law, regulation, legal process or
enforceable governmental request, or (ii) to investigate or enforce
violations of our rights or the security of this site.
</p>
<p>
8.4 Miscellaneous. We reserve the right to share personal
information with the following additional parties: online organizers
using our tools and resellers of our products and services from whose
site the sale originated (even though the sale originates at site of the
reseller, registration and collection of personal information occurs at
this site).
</p>
<p>
<strong> 9. Onward Transfer of Personal Information Outside Your
Country of Residence. </strong>
<br>
Any personal information which we may collect on this site will
be stored and processed in our servers located only in the United
States. By using this site, if you reside outside the United States, you
consent to the transfer of personal information outside your country of
residence to the United States.
</p>
<p>
<strong>10. Security of Personal Information.</strong>
<br>
We follow reasonable and appropriate industry standards to
protect your personal information and data. Unfortunately, no data
transmission over the Internet or method of data storage can be
guaranteed 100% secure. Therefore, while we strive to protect your
personal information by following generally accepted industry standards,
we cannot ensure or warrant the absolute security of any information you
transmit to us or archive at this site.
</p>
<p>
<strong>11. Changing And Updating Personal Information.</strong>
<br>
Upon request, we will permit you to request or make changes or
updates to your personal information for legitimate purposes. We request
identification prior to approving such requests. We reserve the right to
decline any requests that are unreasonably repetitive or systematic,
require unreasonable time or effort of our technical or administrative
personnel, or undermine the privacy rights of others. We reserve the
right to permit you to access your personal information in any account
you establish with this site for purposes of making your own changes or
updates, and in such case, instructions for making such changes or
updates will be provided where necessary.
</p>
<p>
<strong>12. Email From This Site; Opt-Out Rights.</strong>
<br>
If you supply us with your e-mail address you may receive
periodic messages from us with information specific to the Services and
required for the normal functioning of the Services as well as for new
products or services or upcoming events. If you prefer not to receive
periodic email messages, you may opt-out by following the instructions
on the email.
</p>
<p>
<strong>13. Children's Online Policy.</strong>
<br>
We are committed to preserving online privacy for all of its
website visitors, including children. This site is a general audience
site. Consistent with the Children's Online Privacy Protection Act
(COPPA), we will not knowingly collect any information from, or sell to,
children under the age of 13. If you are a parent or guardian who has
discovered that your child under the age of 13 has submitted his or her
personally identifiable information without your permission or consent,
we will remove the information from our active list, at your request. To
request the removal of your child's information, please send contact our
site as provided below under “Contact Us”, and be sure to include in
your message the same login information that your child submitted.
</p>
<p>
<strong> 14. Email And Other Messages Through This Site; ECPA
Notice. </strong>
<br>
This site treats email messages and other electronic messages
that are sent through this site and not viewable by others as
confidential and private, except as required by law, including without
limitation, the Electronic Communications Privacy Act of 1986, 18 U.S.C.
Sections 2701-2711 (the "ECPA"). The ECPA permits this site's limited
ability to intercept and/or disclose electronic messages, for example
(i) as necessary to operate our system or to protect our rights or
property, (ii) upon legal demand (court orders, warrants, subpoenas), or
(iii) where we receive information inadvertently which appears to
pertain to the commission of a crime. This site is not considered a
"secure communications medium" under the ECPA.
</p>
<p>
<strong>15. Contact Us.</strong>
<br>
If you have any questions regarding this Privacy Policy, please
contact the owner and operator of this website business:
</p>
<address>
<strong>OCDevel</strong>
<br>
98 Morrison Ave #2
<br>
Somerville, MA 02144
<br>
Email:
<a href="mailto:tylerrenelle@gmail.com">tylerrenelle@gmail.com</a>
<br>
Telephone: 1-805-975-5236
</address>
<p></p>
<div id="home_static_footer">
© 2012 OCDevel
</div>
</div>
</div>
<app:footer:footer />

74
views/static/team.html Normal file
View file

@ -0,0 +1,74 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | Team
<Head:>
<app:head:head active="team" />
<Body:>
<div id="wrap">
<app:header:header />
<div class='container'>
<section>
<div class="page-header">
<h1>Team</h1>
<p class="lead">The HabitRPG team has developed organically in a meritocratic fashion. We're still figuring it all out, but the members we have listed here have contributed greatly. If you want to be part of the team, contribute to the repository, and if you're a big asset we'll likely adopt you :) See the <a href="https://github.com/lefnire/habitrpg/wiki/Business-Model">Count Me In</a></p>
</div>
</section>
<section>
<div class="page-header">
<h2 id="developers">Developers</h2>
<ul>
<li><a href="https://github.com/lefnire">Tyler Renelle</a></li>
<li><a href="https://github.com/switz">Daniel Saewitz</a></li>
<li><a href="https://github.com/SlappyBag">Stan Lindsey</a></li>
<li><a href="https://github.com/MrConcept">Philip How</a></li>
</ul>
</div>
</section>
<section>
<div class="page-header">
<h2 id="pixel-artists">Pixel Artists</h2>
<ul>
<li><a href="https://github.com/pandoro">Alex Hermans</a></li>
<li><a href="https://github.com/Shaners">Shane Listers</a></li>
</ul>
</div>
</section>
<section>
<div class="page-header">
<h2 id="mobile-developers">Mobile Developers</h2>
<ul>
<li><a href="https://github.com/litenull">Tomaz Korenika</a></li>
<li><a href="https://github.com/lefnire">Tyler Renelle</a></li>
</ul>
</div>
</section>
<section>
<div class="page-header">
<h2 id="designers">Designers</h2>
<ul>
<li><a href="https://github.com/zakkain">Zachary Kain</a></li>
</ul>
</div>
</section>
<section>
<div class="page-header">
<h2 id="community-managers">Community Management</h2>
<ul>
<li><a href="https://github.com/wc8">@wc8</a></li>
<li><a href="https://github.com/horusofoz">Jeff Smith</a></li>
</ul>
</div>
</section>
</div>
</div>
<app:footer:footer />

646
views/static/terms.html Normal file
View file

@ -0,0 +1,646 @@
<import: src="head">
<import: src="header">
<import: src="../app/footer">
<Title:>
HabitRPG | Terms
<Head:>
<app:head:head active="about"/>
<Body:>
<div id="wrap">
<app:header:header />
<div class='container'>
<div class="page-header">
<h1>Terms of Use</h1>
</div>
<p>
Last updated September 2, 2012
</p>
<p>
HabitRPG (or "we") provides services through our
software applications for various devices and platforms ("HabitRPG
Applications") and the HabitRPG.com domain, and any sub domains thereto
(the "Sites"). Individually or collectively, HabitRPG Applications and
Sites may be referred to as the "Services".
</p>
<p>
Please read the following terms and conditions ("Terms of
Service") carefully. These Terms of Service govern your access to and
use of the Services and HabitRPG Content (defined below) and set forth
the legally binding terms for your use of the Services and HabitRPG
Content, whether or not you have registered as a Member.
</p>
<p>
Certain areas of the Services (and your access to or use of
HabitRPG Content) may have different terms and conditions posted or may
require you to agree with and accept additional terms and conditions. If
there is a conflict between these Terms of Service and terms and
conditions posted for a specific area of the Services or HabitRPG
Content, the latter terms and conditions will take precedence with
respect to your use of or access to that area of the Services or HabitRPG
Content.
</p>
<p>
YOU ACKNOWLEDGE AND AGREE THAT, BY CLICKING ON THE "I AGREE" OR
"I ACCEPT" BUTTON, OR BY ACCESSING OR USING THE SERVICES OR BY
DOWNLOADING OR POSTING ANY CONTENT FROM OR ON THE SITES OR THROUGH THE
SERVICES, YOU ARE INDICATING THAT YOU HAVE READ, UNDERSTAND AND AGREE TO
BE BOUND BY THESE TERMS, WHETHER OR NOT YOU HAVE REGISTERED AS A MEMBER,
AND AGREE TO OUR PRIVACY POLICY AS DESCRIBED BELOW. IF YOU DO NOT AGREE
TO THESE TERMS, THEN YOU HAVE NO RIGHT TO ACCESS OR USE THE SERVICES OR
HABITRPG CONTENT.
</p>
<p>
<strong>Modification</strong>
<br>
HabitRPG reserves the right, at its sole discretion, to modify,
discontinue or terminate the Services, including any portion thereof on
a global or individual basis, or to modify these Terms of Service, at
any time and without prior notice. If we modify these Terms of Service,
we will update the "Last Updated Date" above and post the modification
on the Sites and perhaps elsewhere within the Services. By continuing to
access or use the Services after we have posted a modification to these
Terms of Service or have provided you with notice of a modification, you
are indicating that you agree to be bound by the modified Terms of
Service. If the modified Terms of Service are not acceptable to you,
your only recourse is to cease using the Services.
</p>
<p>
<strong>Eligibility and HabitRPG Account Registration</strong>
<br>
In order to access certain features of the Sites and Services, and to
post any Public User Content (defined below) on the Sites or through the
Services, you must register to create an account ("HabitRPG Account") and
become a "Member". In compliance with privacy laws, we do not allow
people below the age of 14 to create accounts; please see our Privacy
Policy for further information. During the registration process, you
will be required to provide certain information and you will establish a
username and a password. You agree to provide accurate, current and
complete information during the registration process and to update such
information to keep it accurate, current and complete. HabitRPG reserves
the right to suspend or terminate your HabitRPG Account if any
information provided during the registration process or thereafter
proves to be inaccurate, not current or incomplete. If you are not a
Member you may browse all areas of the Sites or use the parts of the
Services that are not limited to Members only. You are responsible for
safeguarding your password. You agree not to disclose your password to
any third party and to take sole responsibility for any activities or
actions under your HabitRPG Account, whether or not you have authorized
such activities or actions. You agree to immediately notify HabitRPG of
any unauthorized use of your HabitRPG Account. We are not liable for any
damages or losses caused by someone using your account without your
permission.
</p>
<p>
<strong>Privacy</strong>
<br>
See HabitRPG's Privacy Policy at http://www.HabitRPG.com/privacy for
information and notices concerning HabitRPG's collection and use of your
personal information. If you have any questions about the HabitRPG
Privacy Policy, please contact HabitRPG at privacy AT HabitRPG.com. By
accessing the Services you are agreeing to the terms of our Privacy
Policy.
</p>
<p>
<strong>Content</strong>
<br>
Certain types of content are made available through the Services.
"HabitRPG Content" means, collectively, the text, data, graphics, images,
illustrations, forms and look and feel attributes, HabitRPG trademarks
and logos and other content made available through the Services,
including any technology or code making up the Services, excluding User
Content. "Public User Content" means the text, data, graphics, images, photos,
video or audiovisual content, hypertext links and any other content uploaded,
transmitted or submitted by a Member via the Services with the intent to share
with other users. "Private User Content" means data created through the services
exclusively for personal use or private sharing.
This includes tasks and related data created in HabitRPG Tasks that have not
been explicitly shared publicly.
You understand that by using any of the Services, you may encounter content
that may be deemed offensive, indecent, or objectionable, which content
may or may not be identified as having explicit language, and that the
results of any search or entering of a particular URL may automatically
and unintentionally generate links or references to objectionable
material. Nevertheless, you agree to use the Services at your sole risk
and that we shall not have any liability to you for content that may be
found to be offensive, indecent, or objectionable.
</p>
<p>
<strong>Ownership</strong>
<br>
The Services and HabitRPG Content are protected by copyright, trademark,
and other laws of the United States and foreign countries. Except as
expressly provided in these Terms of Service, HabitRPG and its licensors
exclusively own all right, title and interest in and to the Services and
HabitRPG Content, including all associated intellectual property rights.
You will not remove, alter or obscure any copyright, trademark, service
mark or other proprietary rights notices incorporated in or accompanying
the Services or HabitRPG Content.
</p>
<p>
<strong>HabitRPG License</strong>
<br>
Subject to your compliance with the terms and conditions of these Terms
of Service, HabitRPG grants you a limited, non-exclusive,
non-transferable license, without the right to sublicense, to access,
use, view, download and print, where applicable, the Services and any
HabitRPG Content solely for your personal and non-commercial purposes.
You will not use, copy, adapt, modify, prepare derivative works based
upon, distribute, license, sell, transfer, publicly display, publicly
perform, transmit, stream, broadcast or otherwise exploit the Services
or HabitRPG Content, except as expressly permitted in these Terms of
Service. No licenses or rights are granted to you by implication or
otherwise under any intellectual property rights owned or controlled by
HabitRPG or its licensors, except for the licenses and rights expressly
granted in these Terms of Service. With respect to HabitRPG Applications,
your license is limited to use of such applications on platforms and
devices that you own or control, and you may not distribute or make the
HabitRPG Applications available over a network where it could be used by
multiple devices at the same time.
</p>
<p>
<strong>Public User Content</strong>
<br>
By making available any Public User Content through the Services, you hereby
grant to HabitRPG a worldwide, irrevocable, perpetual, non-exclusive,
transferable, royalty-free license, with the right to sublicense, to
use, copy, adapt, modify, distribute, license, sell, transfer, publicly
display, publicly perform, transmit, stream, broadcast and otherwise
exploit such Public User Content only on, through or by means of the Services.
HabitRPG does not claim any ownership rights in any such Public User Content and
nothing in these Terms of Service will be deemed to restrict any rights
that you may have to use and exploit any such Public User Content.
</p>
<p>
You acknowledge and agree that you are solely responsible for all
Public User Content that you make available through the Services. Accordingly,
you represent and warrant that: (i) you either are the sole and
exclusive owner of all Public User Content that you make available through the
Services or you have all rights, licenses, consents and releases that
are necessary to grant to HabitRPG the rights in such Public User Content, as
contemplated under these Terms of Service; and (ii) neither the User
Content nor your posting, uploading, publication, submission or
transmittal of the Public User Content or HabitRPG's use of the Public User Content (or
any portion thereof) on, through or by means of the Services will
infringe, misappropriate or violate a third party's patent, copyright,
trademark, trade secret, moral rights or other intellectual property
rights, or rights of publicity or privacy, or result in the violation of
any applicable law or regulation.
</p>
<p>
Copyrighted Materials: No Infringing Use. You will not use the
Services to offer, display, distribute, transmit, route, provide
connections to or store any material that infringes copyrighted works or
otherwise violates or promotes the violation of the intellectual
property rights of any third party. HabitRPG has adopted and implemented
a policy that provides for the termination in appropriate circumstances
of the accounts of users who repeatedly infringe or are believed to be
or are charged with repeatedly infringing the rights of copyright
holders.
</p>
<p>
<strong>Notify Us of Infringers</strong>
<br>
If you believe that something on the Services violates your copyright,
notify our copyright agent in writing. The contact information for our
copyright agent is at the bottom of this section.
</p>
<p>
In order for us to take action, you must do the following in your
notice:
</p>
<p>
(1) provide your physical or electronic signature; (2) identify
the copyrighted work that you believe is being infringed; (3) identify
the item on the Services that you think is infringing your work and
include sufficient information about where the material is located on
the Services (including which website and URL) so that we can find it;
(4) provide us with a way to contact you, such as your address,
telephone number, or e-mail; (5) provide a statement that you believe in
good faith that the item you have identified as infringing is not
authorized by the copyright owner, its agent, or the law to be used on
the Services; and (6) provide a statement that the information you
provide in your notice is accurate, and that (under penalty of perjury),
you are authorized to act on behalf of the copyright owner whose work is
being infringed.
</p>
<p>
Here is the contact information for our copyright agent:
</p>
<p>
Copyright Enforcement
<br>
OCDevel
<br>
98 Morrison Ave #2
<br>
Somerville, MA 02144
<br>
Phone: (805) 975-5236
<br>
E-Mail: tylerrenelle at gmail dot com
</p>
<p>
Again, we cannot take action unless you give us all the required
information.
</p>
<p>
<strong>Ratings and Comments &amp; Feedback.</strong>
<br>
You can rate and make comments about content made available through the
Services ("Comments"). HabitRPG advises you to exercise caution and good
judgment when leaving such Comments. Once you complete and submit your
Comments to the Services you will not be able to go back and edit your
Comments. You should also be aware that you could be held legally
responsible for damages to someone's reputation if your Comments are
deemed to be defamatory. Without limiting any other terms of this Terms
of Service, HabitRPG may, but is under no obligation to, monitor or
censor Comments and disclaims any and all liability relating thereto.
Notwithstanding the foregoing, HabitRPG does reserve the right, in its
sole discretion, to remove any Comments that it deems to be improper,
inappropriate or inconsistent with the online activities that are
permitted under these Terms of Service. We welcome and encourage you to
provide feedback, comments and suggestions for improvements to the
Services ("Feedback"). You may submit Feedback by emailing us at support
AT HabitRPG.com. You acknowledge and agree that all Comments and Feedback
will be the sole and exclusive property of HabitRPG and you hereby
irrevocably assign to HabitRPG and agree to irrevocably assign to HabitRPG
all of your right, title, and interest in and to all Comments and
Feedback, including without limitation all worldwide patent rights,
copyright rights, trade secret rights, and other proprietary or
intellectual property rights therein. At HabitRPG's request and expense,
you will execute documents and take such further acts as HabitRPG may
reasonably request to assist HabitRPG to acquire, perfect, and maintain
its intellectual property rights and other legal protections for the
Comments and Feedback.
</p>
<p>
<strong>Interactions between Users</strong>
<br>
You are solely responsible for your interactions (including any
disputes) with other users. You understand that HabitRPG does not in any
way screen HabitRPG users, except to only allow people aged 14 and over
to create accounts. You are solely responsible for, and will exercise
caution, discretion, common sense and judgment in, using the Services
and disclosing personal information to other HabitRPG users. You agree to
take reasonable precautions in all interactions with other HabitRPG
users, particularly if you decide to meet a HabitRPG user offline, or in
person. Your use of the Services, HabitRPG Content and any other content
made available through the Services is at your sole risk and discretion
and HabitRPG hereby disclaims any and all liability to you or any third
party relating thereto. HabitRPG reserves the right to contact Members,
in compliance with applicable law, in order to evaluate compliance with
the rules and policies in these Terms of Service. You will cooperate
fully with HabitRPG to investigate any suspected unlawful, fraudulent or
improper activity, including, without limitation, granting authorized
HabitRPG representatives access to any password-protected portions of
your HabitRPG Account.
</p>
<p>
<strong>General Prohibitions</strong>
<br>
You agree not to do any of the following while using the Services or
HabitRPG Content:
<br>
</p>
<ul>
<li>
Post, upload, publish, submit or transmit any text, graphics,
images, software, music, audio, video, information or other material
that: (i) infringes, misappropriates or violates a third party's
patent, copyright, trademark, trade secret, moral rights or other
intellectual property rights, or rights of publicity or privacy; (ii)
violates, or encourages any conduct that would violate, any applicable
law or regulation or would give rise to civil liability; (iii) is
fraudulent, false, misleading or deceptive; (iv) is defamatory,
obscene, pornographic, vulgar or offensive; (v) promotes
discrimination, bigotry, racism, hatred, harassment or harm against any
individual or group; (vi) is violent or threatening or promotes
violence or actions that are threatening to any other person; or (vii)
promotes illegal or harmful activities or substances (including but not
limited to activities that promote or provide instructional information
regarding the manufacture or purchase of illegal weapons or illegal
substances).
</li>
<li>
Use, display, mirror, frame or utilize framing techniques to
enclose the Services, or any individual element or materials within the
Services, HabitRPG's name, any HabitRPG trademark, logo or other
proprietary information, the content of any text or the layout and
design of any page or form contained on a page, without HabitRPG's
express written consent;
</li>
<li>
Access, tamper with, or use non-public areas of the Services,
HabitRPG's computer systems, or the technical delivery systems of
HabitRPG's providers;
</li>
<li>
Attempt to probe, scan, or test the vulnerability of any
HabitRPG system or network or breach any security or authentication
measures;
</li>
<li>
Avoid, bypass, remove, deactivate, impair, descramble or
otherwise circumvent any technological measure implemented by HabitRPG
or any of HabitRPG's providers or any other third party (including
another user) to protect the Services or HabitRPG Content;
</li>
<li>
Attempt to access or search the Services or HabitRPG Content or
download HabitRPG Content from the Services through the use of any
engine, software, tool, agent, device or mechanism (including spiders,
robots, crawlers, data mining tools or the like) other than the
software and/or search agents provided by HabitRPG or other generally
available third party web browsers (such as Google Chrome, Microsoft
Internet Explorer, Mozilla Firefox, Safari or Opera);
</li>
<li>
Send any unsolicited or unauthorized advertising, promotional
materials, email, junk mail, spam, chain letters or other form of
solicitation;
</li>
<li>
Use any meta tags or other hidden text or metadata utilizing a
HabitRPG trademark, logo URL or product name without HabitRPG's express
written consent;
</li>
<li>
Use the Services or HabitRPG Content for any commercial purpose
or the benefit of any third party or in any manner not permitted by
these Terms of Service;
</li>
<li>
Forge any TCP/IP packet header or any part of the header
information in any email or newsgroup posting, or in any way use the
Services or HabitRPG Content to send altered, deceptive or false
source-identifying information;
</li>
<li>
Attempt to decipher, decompile, disassemble or reverse
engineer any of the software used to provide the Services or HabitRPG
Content;
</li>
<li>
Interfere with, or attempt to interfere with, the access of
any user, host or network, including, without limitation, sending a
virus, overloading, flooding, spamming, or mail-bombing the Services;
</li>
<li>
Collect or store any personally identifiable information from
the Services from other users of the Services without their express
permission;
</li>
<li>
Impersonate or misrepresent your affiliation with any person
or entity; Violate any applicable law or regulation; or
</li>
<li>
Encourage or enable any other individual to do any of the
foregoing.
</li>
</ul>
<p>
HabitRPG will have the right to investigate and prosecute
violations of any of the above, including intellectual property rights
infringement and Services security issues, to the fullest extent of the
law. HabitRPG may involve and cooperate with law enforcement authorities
in prosecuting users who violate these Terms of Service. You acknowledge
that HabitRPG has no obligation to monitor your access to or use of the
Services or HabitRPG Content or to review or edit any Public User Content, but
has the right to do so for the purpose of operating the Services, to
ensure your compliance with these Terms of Service, or to comply with
applicable law or the order or requirement of a court, administrative
agency or other governmental body. HabitRPG reserves the right, at any
time and without prior notice, to remove or disable access to any
HabitRPG Content, including, any Public User Content, that HabitRPG, in its sole
discretion, considers to be in violation of these Terms of Service or
otherwise harmful to the Services.
</p>
<p>
<strong>Links</strong>
<br>
The Services may contain links to third-party websites or resources. You
acknowledge and agree that HabitRPG is not responsible or liable for: (i)
the availability or accuracy of such websites or resources; or (ii) the
content, products, or services on or available from such websites or
resources. Links to such websites or resources do not imply any
endorsement by HabitRPG of such websites or resources or the content,
products, or services available from such websites or resources. You
acknowledge sole responsibility for and assume all risk arising from
your use of any such websites or resources.
</p>
<p>
<strong>Termination and HabitRPG Account; Cancellation</strong>
<br>
Without limiting other remedies, HabitRPG may at any time suspend or
terminate your HabitRPG Account and refuse to provide access to the
Services. In addition, HabitRPG may notify authorities or take any
actions it deems appropriate, without notice to you, if HabitRPG suspects
or determines, in its own discretion, that you may have or there is a
significant risk that you have (i) failed to comply with any provision
of these Terms of Service or any policies or rules established by
HabitRPG; or (ii) engaged in actions relating to or in the course of
using the Services that may be illegal or cause liability, harm,
embarrassment, harassment, abuse or disruption for you, HabitRPG Users,
HabitRPG or any other third parties or the Services.
</p>
<p>
You may terminate your HabitRPG Account at any time and for any
reason by sending email to support AT HabitRPG.com. Upon any termination
by a Member, the related account will no longer be accessible.
</p>
<p>
After any termination, you understand and acknowledge that we
will have no further obligation to provide the Services and all licenses
and other rights granted to you by these Terms of Service will
immediately cease. HabitRPG will not be liable to you or any third party
for termination of the Services or termination of your use of either.
UPON ANY TERMINATION OR SUSPENSION, ANY CONTENT, MATERIALS OR
INFORMATION (INCLUDING PUBLIC USER CONTENT) THAT YOU HAVE SUBMITTED ON THE
SERVICES OR THAT WHICH IS RELATED TO YOUR ACCOUNT MAY NO LONGER BE
ACCESSED BY YOU. Furthermore, HabitRPG will have no obligation to
maintain any information stored in our database related to your account
or to forward any information to you or any third party.
</p>
<p>
Any suspension, termination or cancellation will not affect your
obligations to HabitRPG under these Terms of Service (including, without
limitation, proprietary rights and ownership, indemnification and
limitation of liability), which by their sense and context are intended
to survive such suspension, termination or cancellation.
</p>
<p>
<strong>Disclaimers</strong>
<br>
THE SERVICES, HABITRPG CONTENT AND PUBLIC USER CONTENT ARE PROVIDED "AS IS",
WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED. WITHOUT
LIMITING THE FOREGOING, HABITRPG EXPLICITLY DISCLAIMS ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR
NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR
USAGE OF TRADE.
</p>
<p>
HABITRPG MAKES NO WARRANTY THAT THE SERVICES, HABITRPG CONTENT OR
PUBLIC USER CONTENT WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN
UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS. HABITRPG MAKES NO WARRANTY
REGARDING THE QUALITY OF ANY PRODUCTS, SERVICES OR CONTENT PURCHASED OR
OBTAINED THROUGH THE SERVICES OR THE ACCURACY, TIMELINESS, TRUTHFULNESS,
COMPLETENESS OR RELIABILITY OF ANY CONTENT OBTAINED THROUGH THE
SERVICES.
</p>
<p>
NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM
HABITRPG OR THROUGH THE SERVICES, HABITRPG CONTENT OR PUBLIC USER CONTENT, WILL
CREATE ANY WARRANTY NOT EXPRESSLY MADE HEREIN.
</p>
<p>
<strong>Indemnity</strong>
<br>
You agree to defend, indemnify, and hold HabitRPG, its officers,
directors, employees and agents, harmless from and against any claims,
liabilities, damages, losses, and expenses, including, without
limitation, reasonable legal and accounting fees, arising out of or in
any way connected with Public User Content you submit to HabitRPG, your access
to or use of the Services or HabitRPG Content, or your violation of these
Terms of Service.
</p>
<p>
<strong>Limitation of Liability</strong>
<br>
YOU ACKNOWLEDGE AND AGREE THAT, TO THE MAXIMUM EXTENT PERMITTED BY LAW,
THE ENTIRE RISK ARISING OUT OF YOUR ACCESS TO AND USE OF THE SERVICES
AND CONTENT THEREIN REMAINS WITH YOU. NEITHER HABITRPG NOR ANY OTHER
PARTY INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE SERVICES OR
HABITRPG CONTENT WILL BE LIABLE FOR ANY INCIDENTAL, SPECIAL, EXEMPLARY OR
CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, LOSS OF DATA OR LOSS OF
GOODWILL, SERVICE INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE
COST OF SUBSTITUTE PRODUCTS OR SERVICES, ARISING OUT OF OR IN CONNECTION
WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE THE SERVICES OR
CONTENT THEREIN, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING
NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER LEGAL THEORY, AND WHETHER OR
NOT HABITRPG HAS BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGE, EVEN IF
A LIMITED REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED OF ITS
ESSENTIAL PURPOSE. YOU SPECIFICALLY ACKNOWLEDGE THAT HABITRPG IS NOT
LIABLE FOR THE DEFAMATORY, OFFENSIVE OR ILLEGAL CONDUCT OF OTHER USERS
OR THIRD PARTIES AND THAT THE RISK OF INJURY FROM THE FOREGOING RESTS
ENTIRELY WITH YOU. FURTHER, HABITRPG WILL HAVE NO LIABILITY TO YOU OR TO
ANY THIRD PARTY FOR ANY PUBLIC USER CONTENT OR THIRD-PARTY CONTENT UPLOADED
ONTO OR DOWNLOADED FROM THE SITES OR THROUGH THE SERVICES.
</p>
<p>
IN NO EVENT WILL HABITRPG'S AGGREGATE LIABILITY ARISING OUT OF OR
IN CONNECTION WITH THESE TERMS OF SERVICE OR FROM THE USE OF OR
INABILITY TO USE THE SITE, SERVICES OR CONTENT THEREIN EXCEED ONE
HUNDRED U.S. DOLLARS ($100). THE LIMITATIONS OF DAMAGES SET FORTH ABOVE
ARE FUNDAMENTAL ELEMENTS OF THE BASIS OF THE BARGAIN BETWEEN HABITRPG AND
YOU. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF
LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, SO THE ABOVE
LIMITATION MAY NOT APPLY TO YOU.
</p>
<p>
<strong>Proprietary Rights Notices</strong>
<br>
All trademarks, service marks, logos, trade names and any other
proprietary designations of HabitRPG used herein are trademarks or
registered trademarks of HabitRPG. Any other trademarks, service marks,
logos, trade names and any other proprietary designations are the
trademarks or registered trademarks of their respective parties.
</p>
<p>
<strong>Controlling Law and Jurisdiction</strong>
<br>
These Terms of Service and any action related thereto will be governed
by the laws of the State of California without regard to its conflict of
laws provisions. The exclusive jurisdiction and venue of any action with
respect to the subject matter of these Terms of Service will be the
courts having jurisdiction over disputes arising in Santa Clara County,
California, and each of the parties hereto waives any objection to
jurisdiction and venue in such courts.
</p>
<p>
YOU AGREE THAT IF YOU WANT TO SUE US, YOU MUST FILE YOUR LAWSUIT
WITHIN ONE YEAR AFTER THE EVENT THAT GAVE RISE TO YOUR LAWSUIT.
OTHERWISE, YOUR LAWSUIT WILL BE PERMANENTLY BARRED.
</p>
<p>
<strong>Export Control</strong>
<br>
You may not use or otherwise export or re-export the Services except as
authorized by United States law and the laws of the jurisdiction in
which the Services were obtained. In particular, but without limitation,
the Services may not be exported or re-exported (a) into any U.S.
embargoed countries or (b) to anyone on the U.S. Treasury Department's
list of Specially Designated Nationals or the U.S. Department of
Commerce Denied Person's List or Entity List. By using the Services, you
represent and warrant that you are not located in any such country or on
any such list. You also agree that you will not use these products for
any purposes prohibited by United States law, including, without
limitation, the development, design, manufacture or production of
nuclear, missiles, or chemical or biological weapons.
</p>
<p>
<strong>Entire Agreement</strong>
<br>
These Terms of Service constitute the entire and exclusive understanding
and agreement between HabitRPG and you regarding the Services and HabitRPG
Content, and these Terms of Service supersede and replace any and all
prior oral or written understandings or agreements between HabitRPG and
you regarding the Services and HabitRPG Content.
</p>
<p>
<strong>Assignment</strong>
<br>
You may not assign or transfer these Terms of Service, by operation of
law or otherwise, without HabitRPG's prior written consent. Any attempt
by you to assign or transfer these Terms of Service, without such
consent, will be null and of no effect. HabitRPG may freely assign these
Terms of Service. Subject to the foregoing, these Terms of Service will
bind and inure to the benefit of the parties, their successors and
permitted assigns.
</p>
<p>
<strong>Notices</strong>
<br>
You consent to the use of: (i) electronic means to complete these Terms
of Service and to deliver any notices or other communications permitted
or required hereunder; and (ii) electronic records to store information
related to these Terms of Service or your use of the Services. Any
notices or other communications permitted to required hereunder,
including those regarding modifications to these Terms of Service, will
be in writing and given: (x) by HabitRPG via email (in each case to the
address that you provide) or (y) by posting to the Sites or Services.
For notices made by e-mail, the date of receipt will be deemed the date
on which such notice is transmitted.
</p>
<p>
<strong>General</strong>
<br>
The failure of HabitRPG to enforce any right or provision of these Terms
of Service will not constitute a waiver of future enforcement of that
right or provision. The waiver of any such right or provision will be
effective only if in writing and signed by a duly authorized
representative of HabitRPG. Except as expressly set forth in these Terms
of Service, the exercise by either party of any of its remedies under
these Terms of Service will be without prejudice to its other remedies
under these Terms of Service or otherwise. If for any reason a court of
competent jurisdiction finds any provision of these Terms of Service
invalid or unenforceable, that provision will be enforced to the maximum
extent permissible and the other provisions of these Terms of Service
will remain in full force and effect.
</p>
<p>
<strong>Contacting Us</strong>
<br>
If you have any questions about these Terms of Service, please contact
us at tylerrenelle@gmail.com.
</p>
<div id="home_static_footer">
© 2012 OCDevel
</div>
</div>
</div>
<app:footer:footer />

View file

@ -1,636 +0,0 @@
<Title:>
Terms Of Use
<Body:>
<div class="well">
<a href="/">&laquo; Back</a>
<div class="page-header">
<h1>Terms Of Use</h1>
</div>
<p>
Last updated September 2, 2012
</p>
<p>
HabitRPG (or "we") provides services through our
software applications for various devices and platforms ("HabitRPG
Applications") and the HabitRPG.com domain, and any sub domains thereto
(the "Sites"). Individually or collectively, HabitRPG Applications and
Sites may be referred to as the "Services".
</p>
<p>
Please read the following terms and conditions ("Terms of
Service") carefully. These Terms of Service govern your access to and
use of the Services and HabitRPG Content (defined below) and set forth
the legally binding terms for your use of the Services and HabitRPG
Content, whether or not you have registered as a Member.
</p>
<p>
Certain areas of the Services (and your access to or use of
HabitRPG Content) may have different terms and conditions posted or may
require you to agree with and accept additional terms and conditions. If
there is a conflict between these Terms of Service and terms and
conditions posted for a specific area of the Services or HabitRPG
Content, the latter terms and conditions will take precedence with
respect to your use of or access to that area of the Services or HabitRPG
Content.
</p>
<p>
YOU ACKNOWLEDGE AND AGREE THAT, BY CLICKING ON THE "I AGREE" OR
"I ACCEPT" BUTTON, OR BY ACCESSING OR USING THE SERVICES OR BY
DOWNLOADING OR POSTING ANY CONTENT FROM OR ON THE SITES OR THROUGH THE
SERVICES, YOU ARE INDICATING THAT YOU HAVE READ, UNDERSTAND AND AGREE TO
BE BOUND BY THESE TERMS, WHETHER OR NOT YOU HAVE REGISTERED AS A MEMBER,
AND AGREE TO OUR PRIVACY POLICY AS DESCRIBED BELOW. IF YOU DO NOT AGREE
TO THESE TERMS, THEN YOU HAVE NO RIGHT TO ACCESS OR USE THE SERVICES OR
HABITRPG CONTENT.
</p>
<p>
<strong>Modification</strong>
<br>
HabitRPG reserves the right, at its sole discretion, to modify,
discontinue or terminate the Services, including any portion thereof on
a global or individual basis, or to modify these Terms of Service, at
any time and without prior notice. If we modify these Terms of Service,
we will update the "Last Updated Date" above and post the modification
on the Sites and perhaps elsewhere within the Services. By continuing to
access or use the Services after we have posted a modification to these
Terms of Service or have provided you with notice of a modification, you
are indicating that you agree to be bound by the modified Terms of
Service. If the modified Terms of Service are not acceptable to you,
your only recourse is to cease using the Services.
</p>
<p>
<strong>Eligibility and HabitRPG Account Registration</strong>
<br>
In order to access certain features of the Sites and Services, and to
post any Public User Content (defined below) on the Sites or through the
Services, you must register to create an account ("HabitRPG Account") and
become a "Member". In compliance with privacy laws, we do not allow
people below the age of 14 to create accounts; please see our Privacy
Policy for further information. During the registration process, you
will be required to provide certain information and you will establish a
username and a password. You agree to provide accurate, current and
complete information during the registration process and to update such
information to keep it accurate, current and complete. HabitRPG reserves
the right to suspend or terminate your HabitRPG Account if any
information provided during the registration process or thereafter
proves to be inaccurate, not current or incomplete. If you are not a
Member you may browse all areas of the Sites or use the parts of the
Services that are not limited to Members only. You are responsible for
safeguarding your password. You agree not to disclose your password to
any third party and to take sole responsibility for any activities or
actions under your HabitRPG Account, whether or not you have authorized
such activities or actions. You agree to immediately notify HabitRPG of
any unauthorized use of your HabitRPG Account. We are not liable for any
damages or losses caused by someone using your account without your
permission.
</p>
<p>
<strong>Privacy</strong>
<br>
See HabitRPG's Privacy Policy at http://www.HabitRPG.com/privacy for
information and notices concerning HabitRPG's collection and use of your
personal information. If you have any questions about the HabitRPG
Privacy Policy, please contact HabitRPG at privacy AT HabitRPG.com. By
accessing the Services you are agreeing to the terms of our Privacy
Policy.
</p>
<p>
<strong>Content</strong>
<br>
Certain types of content are made available through the Services.
"HabitRPG Content" means, collectively, the text, data, graphics, images,
illustrations, forms and look and feel attributes, HabitRPG trademarks
and logos and other content made available through the Services,
including any technology or code making up the Services, excluding User
Content. "Public User Content" means the text, data, graphics, images, photos,
video or audiovisual content, hypertext links and any other content uploaded,
transmitted or submitted by a Member via the Services with the intent to share
with other users. "Private User Content" means data created through the services
exclusively for personal use or private sharing.
This includes tasks and related data created in HabitRPG Tasks that have not
been explicitly shared publicly.
You understand that by using any of the Services, you may encounter content
that may be deemed offensive, indecent, or objectionable, which content
may or may not be identified as having explicit language, and that the
results of any search or entering of a particular URL may automatically
and unintentionally generate links or references to objectionable
material. Nevertheless, you agree to use the Services at your sole risk
and that we shall not have any liability to you for content that may be
found to be offensive, indecent, or objectionable.
</p>
<p>
<strong>Ownership</strong>
<br>
The Services and HabitRPG Content are protected by copyright, trademark,
and other laws of the United States and foreign countries. Except as
expressly provided in these Terms of Service, HabitRPG and its licensors
exclusively own all right, title and interest in and to the Services and
HabitRPG Content, including all associated intellectual property rights.
You will not remove, alter or obscure any copyright, trademark, service
mark or other proprietary rights notices incorporated in or accompanying
the Services or HabitRPG Content.
</p>
<p>
<strong>HabitRPG License</strong>
<br>
Subject to your compliance with the terms and conditions of these Terms
of Service, HabitRPG grants you a limited, non-exclusive,
non-transferable license, without the right to sublicense, to access,
use, view, download and print, where applicable, the Services and any
HabitRPG Content solely for your personal and non-commercial purposes.
You will not use, copy, adapt, modify, prepare derivative works based
upon, distribute, license, sell, transfer, publicly display, publicly
perform, transmit, stream, broadcast or otherwise exploit the Services
or HabitRPG Content, except as expressly permitted in these Terms of
Service. No licenses or rights are granted to you by implication or
otherwise under any intellectual property rights owned or controlled by
HabitRPG or its licensors, except for the licenses and rights expressly
granted in these Terms of Service. With respect to HabitRPG Applications,
your license is limited to use of such applications on platforms and
devices that you own or control, and you may not distribute or make the
HabitRPG Applications available over a network where it could be used by
multiple devices at the same time.
</p>
<p>
<strong>Public User Content</strong>
<br>
By making available any Public User Content through the Services, you hereby
grant to HabitRPG a worldwide, irrevocable, perpetual, non-exclusive,
transferable, royalty-free license, with the right to sublicense, to
use, copy, adapt, modify, distribute, license, sell, transfer, publicly
display, publicly perform, transmit, stream, broadcast and otherwise
exploit such Public User Content only on, through or by means of the Services.
HabitRPG does not claim any ownership rights in any such Public User Content and
nothing in these Terms of Service will be deemed to restrict any rights
that you may have to use and exploit any such Public User Content.
</p>
<p>
You acknowledge and agree that you are solely responsible for all
Public User Content that you make available through the Services. Accordingly,
you represent and warrant that: (i) you either are the sole and
exclusive owner of all Public User Content that you make available through the
Services or you have all rights, licenses, consents and releases that
are necessary to grant to HabitRPG the rights in such Public User Content, as
contemplated under these Terms of Service; and (ii) neither the User
Content nor your posting, uploading, publication, submission or
transmittal of the Public User Content or HabitRPG's use of the Public User Content (or
any portion thereof) on, through or by means of the Services will
infringe, misappropriate or violate a third party's patent, copyright,
trademark, trade secret, moral rights or other intellectual property
rights, or rights of publicity or privacy, or result in the violation of
any applicable law or regulation.
</p>
<p>
Copyrighted Materials: No Infringing Use. You will not use the
Services to offer, display, distribute, transmit, route, provide
connections to or store any material that infringes copyrighted works or
otherwise violates or promotes the violation of the intellectual
property rights of any third party. HabitRPG has adopted and implemented
a policy that provides for the termination in appropriate circumstances
of the accounts of users who repeatedly infringe or are believed to be
or are charged with repeatedly infringing the rights of copyright
holders.
</p>
<p>
<strong>Notify Us of Infringers</strong>
<br>
If you believe that something on the Services violates your copyright,
notify our copyright agent in writing. The contact information for our
copyright agent is at the bottom of this section.
</p>
<p>
In order for us to take action, you must do the following in your
notice:
</p>
<p>
(1) provide your physical or electronic signature; (2) identify
the copyrighted work that you believe is being infringed; (3) identify
the item on the Services that you think is infringing your work and
include sufficient information about where the material is located on
the Services (including which website and URL) so that we can find it;
(4) provide us with a way to contact you, such as your address,
telephone number, or e-mail; (5) provide a statement that you believe in
good faith that the item you have identified as infringing is not
authorized by the copyright owner, its agent, or the law to be used on
the Services; and (6) provide a statement that the information you
provide in your notice is accurate, and that (under penalty of perjury),
you are authorized to act on behalf of the copyright owner whose work is
being infringed.
</p>
<p>
Here is the contact information for our copyright agent:
</p>
<p>
Copyright Enforcement
<br>
OCDevel
<br>
98 Morrison Ave #2
<br>
Somerville, MA 02144
<br>
Phone: (805) 975-5236
<br>
E-Mail: tylerrenelle at gmail dot com
</p>
<p>
Again, we cannot take action unless you give us all the required
information.
</p>
<p>
<strong>Ratings and Comments &amp; Feedback.</strong>
<br>
You can rate and make comments about content made available through the
Services ("Comments"). HabitRPG advises you to exercise caution and good
judgment when leaving such Comments. Once you complete and submit your
Comments to the Services you will not be able to go back and edit your
Comments. You should also be aware that you could be held legally
responsible for damages to someone's reputation if your Comments are
deemed to be defamatory. Without limiting any other terms of this Terms
of Service, HabitRPG may, but is under no obligation to, monitor or
censor Comments and disclaims any and all liability relating thereto.
Notwithstanding the foregoing, HabitRPG does reserve the right, in its
sole discretion, to remove any Comments that it deems to be improper,
inappropriate or inconsistent with the online activities that are
permitted under these Terms of Service. We welcome and encourage you to
provide feedback, comments and suggestions for improvements to the
Services ("Feedback"). You may submit Feedback by emailing us at support
AT HabitRPG.com. You acknowledge and agree that all Comments and Feedback
will be the sole and exclusive property of HabitRPG and you hereby
irrevocably assign to HabitRPG and agree to irrevocably assign to HabitRPG
all of your right, title, and interest in and to all Comments and
Feedback, including without limitation all worldwide patent rights,
copyright rights, trade secret rights, and other proprietary or
intellectual property rights therein. At HabitRPG's request and expense,
you will execute documents and take such further acts as HabitRPG may
reasonably request to assist HabitRPG to acquire, perfect, and maintain
its intellectual property rights and other legal protections for the
Comments and Feedback.
</p>
<p>
<strong>Interactions between Users</strong>
<br>
You are solely responsible for your interactions (including any
disputes) with other users. You understand that HabitRPG does not in any
way screen HabitRPG users, except to only allow people aged 14 and over
to create accounts. You are solely responsible for, and will exercise
caution, discretion, common sense and judgment in, using the Services
and disclosing personal information to other HabitRPG users. You agree to
take reasonable precautions in all interactions with other HabitRPG
users, particularly if you decide to meet a HabitRPG user offline, or in
person. Your use of the Services, HabitRPG Content and any other content
made available through the Services is at your sole risk and discretion
and HabitRPG hereby disclaims any and all liability to you or any third
party relating thereto. HabitRPG reserves the right to contact Members,
in compliance with applicable law, in order to evaluate compliance with
the rules and policies in these Terms of Service. You will cooperate
fully with HabitRPG to investigate any suspected unlawful, fraudulent or
improper activity, including, without limitation, granting authorized
HabitRPG representatives access to any password-protected portions of
your HabitRPG Account.
</p>
<p>
<strong>General Prohibitions</strong>
<br>
You agree not to do any of the following while using the Services or
HabitRPG Content:
<br>
</p>
<ul>
<li>
Post, upload, publish, submit or transmit any text, graphics,
images, software, music, audio, video, information or other material
that: (i) infringes, misappropriates or violates a third party's
patent, copyright, trademark, trade secret, moral rights or other
intellectual property rights, or rights of publicity or privacy; (ii)
violates, or encourages any conduct that would violate, any applicable
law or regulation or would give rise to civil liability; (iii) is
fraudulent, false, misleading or deceptive; (iv) is defamatory,
obscene, pornographic, vulgar or offensive; (v) promotes
discrimination, bigotry, racism, hatred, harassment or harm against any
individual or group; (vi) is violent or threatening or promotes
violence or actions that are threatening to any other person; or (vii)
promotes illegal or harmful activities or substances (including but not
limited to activities that promote or provide instructional information
regarding the manufacture or purchase of illegal weapons or illegal
substances).
</li>
<li>
Use, display, mirror, frame or utilize framing techniques to
enclose the Services, or any individual element or materials within the
Services, HabitRPG's name, any HabitRPG trademark, logo or other
proprietary information, the content of any text or the layout and
design of any page or form contained on a page, without HabitRPG's
express written consent;
</li>
<li>
Access, tamper with, or use non-public areas of the Services,
HabitRPG's computer systems, or the technical delivery systems of
HabitRPG's providers;
</li>
<li>
Attempt to probe, scan, or test the vulnerability of any
HabitRPG system or network or breach any security or authentication
measures;
</li>
<li>
Avoid, bypass, remove, deactivate, impair, descramble or
otherwise circumvent any technological measure implemented by HabitRPG
or any of HabitRPG's providers or any other third party (including
another user) to protect the Services or HabitRPG Content;
</li>
<li>
Attempt to access or search the Services or HabitRPG Content or
download HabitRPG Content from the Services through the use of any
engine, software, tool, agent, device or mechanism (including spiders,
robots, crawlers, data mining tools or the like) other than the
software and/or search agents provided by HabitRPG or other generally
available third party web browsers (such as Google Chrome, Microsoft
Internet Explorer, Mozilla Firefox, Safari or Opera);
</li>
<li>
Send any unsolicited or unauthorized advertising, promotional
materials, email, junk mail, spam, chain letters or other form of
solicitation;
</li>
<li>
Use any meta tags or other hidden text or metadata utilizing a
HabitRPG trademark, logo URL or product name without HabitRPG's express
written consent;
</li>
<li>
Use the Services or HabitRPG Content for any commercial purpose
or the benefit of any third party or in any manner not permitted by
these Terms of Service;
</li>
<li>
Forge any TCP/IP packet header or any part of the header
information in any email or newsgroup posting, or in any way use the
Services or HabitRPG Content to send altered, deceptive or false
source-identifying information;
</li>
<li>
Attempt to decipher, decompile, disassemble or reverse
engineer any of the software used to provide the Services or HabitRPG
Content;
</li>
<li>
Interfere with, or attempt to interfere with, the access of
any user, host or network, including, without limitation, sending a
virus, overloading, flooding, spamming, or mail-bombing the Services;
</li>
<li>
Collect or store any personally identifiable information from
the Services from other users of the Services without their express
permission;
</li>
<li>
Impersonate or misrepresent your affiliation with any person
or entity; Violate any applicable law or regulation; or
</li>
<li>
Encourage or enable any other individual to do any of the
foregoing.
</li>
</ul>
<p>
HabitRPG will have the right to investigate and prosecute
violations of any of the above, including intellectual property rights
infringement and Services security issues, to the fullest extent of the
law. HabitRPG may involve and cooperate with law enforcement authorities
in prosecuting users who violate these Terms of Service. You acknowledge
that HabitRPG has no obligation to monitor your access to or use of the
Services or HabitRPG Content or to review or edit any Public User Content, but
has the right to do so for the purpose of operating the Services, to
ensure your compliance with these Terms of Service, or to comply with
applicable law or the order or requirement of a court, administrative
agency or other governmental body. HabitRPG reserves the right, at any
time and without prior notice, to remove or disable access to any
HabitRPG Content, including, any Public User Content, that HabitRPG, in its sole
discretion, considers to be in violation of these Terms of Service or
otherwise harmful to the Services.
</p>
<p>
<strong>Links</strong>
<br>
The Services may contain links to third-party websites or resources. You
acknowledge and agree that HabitRPG is not responsible or liable for: (i)
the availability or accuracy of such websites or resources; or (ii) the
content, products, or services on or available from such websites or
resources. Links to such websites or resources do not imply any
endorsement by HabitRPG of such websites or resources or the content,
products, or services available from such websites or resources. You
acknowledge sole responsibility for and assume all risk arising from
your use of any such websites or resources.
</p>
<p>
<strong>Termination and HabitRPG Account; Cancellation</strong>
<br>
Without limiting other remedies, HabitRPG may at any time suspend or
terminate your HabitRPG Account and refuse to provide access to the
Services. In addition, HabitRPG may notify authorities or take any
actions it deems appropriate, without notice to you, if HabitRPG suspects
or determines, in its own discretion, that you may have or there is a
significant risk that you have (i) failed to comply with any provision
of these Terms of Service or any policies or rules established by
HabitRPG; or (ii) engaged in actions relating to or in the course of
using the Services that may be illegal or cause liability, harm,
embarrassment, harassment, abuse or disruption for you, HabitRPG Users,
HabitRPG or any other third parties or the Services.
</p>
<p>
You may terminate your HabitRPG Account at any time and for any
reason by sending email to support AT HabitRPG.com. Upon any termination
by a Member, the related account will no longer be accessible.
</p>
<p>
After any termination, you understand and acknowledge that we
will have no further obligation to provide the Services and all licenses
and other rights granted to you by these Terms of Service will
immediately cease. HabitRPG will not be liable to you or any third party
for termination of the Services or termination of your use of either.
UPON ANY TERMINATION OR SUSPENSION, ANY CONTENT, MATERIALS OR
INFORMATION (INCLUDING PUBLIC USER CONTENT) THAT YOU HAVE SUBMITTED ON THE
SERVICES OR THAT WHICH IS RELATED TO YOUR ACCOUNT MAY NO LONGER BE
ACCESSED BY YOU. Furthermore, HabitRPG will have no obligation to
maintain any information stored in our database related to your account
or to forward any information to you or any third party.
</p>
<p>
Any suspension, termination or cancellation will not affect your
obligations to HabitRPG under these Terms of Service (including, without
limitation, proprietary rights and ownership, indemnification and
limitation of liability), which by their sense and context are intended
to survive such suspension, termination or cancellation.
</p>
<p>
<strong>Disclaimers</strong>
<br>
THE SERVICES, HABITRPG CONTENT AND PUBLIC USER CONTENT ARE PROVIDED "AS IS",
WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED. WITHOUT
LIMITING THE FOREGOING, HABITRPG EXPLICITLY DISCLAIMS ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR
NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR
USAGE OF TRADE.
</p>
<p>
HABITRPG MAKES NO WARRANTY THAT THE SERVICES, HABITRPG CONTENT OR
PUBLIC USER CONTENT WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN
UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS. HABITRPG MAKES NO WARRANTY
REGARDING THE QUALITY OF ANY PRODUCTS, SERVICES OR CONTENT PURCHASED OR
OBTAINED THROUGH THE SERVICES OR THE ACCURACY, TIMELINESS, TRUTHFULNESS,
COMPLETENESS OR RELIABILITY OF ANY CONTENT OBTAINED THROUGH THE
SERVICES.
</p>
<p>
NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM
HABITRPG OR THROUGH THE SERVICES, HABITRPG CONTENT OR PUBLIC USER CONTENT, WILL
CREATE ANY WARRANTY NOT EXPRESSLY MADE HEREIN.
</p>
<p>
<strong>Indemnity</strong>
<br>
You agree to defend, indemnify, and hold HabitRPG, its officers,
directors, employees and agents, harmless from and against any claims,
liabilities, damages, losses, and expenses, including, without
limitation, reasonable legal and accounting fees, arising out of or in
any way connected with Public User Content you submit to HabitRPG, your access
to or use of the Services or HabitRPG Content, or your violation of these
Terms of Service.
</p>
<p>
<strong>Limitation of Liability</strong>
<br>
YOU ACKNOWLEDGE AND AGREE THAT, TO THE MAXIMUM EXTENT PERMITTED BY LAW,
THE ENTIRE RISK ARISING OUT OF YOUR ACCESS TO AND USE OF THE SERVICES
AND CONTENT THEREIN REMAINS WITH YOU. NEITHER HABITRPG NOR ANY OTHER
PARTY INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE SERVICES OR
HABITRPG CONTENT WILL BE LIABLE FOR ANY INCIDENTAL, SPECIAL, EXEMPLARY OR
CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, LOSS OF DATA OR LOSS OF
GOODWILL, SERVICE INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE
COST OF SUBSTITUTE PRODUCTS OR SERVICES, ARISING OUT OF OR IN CONNECTION
WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE THE SERVICES OR
CONTENT THEREIN, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING
NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER LEGAL THEORY, AND WHETHER OR
NOT HABITRPG HAS BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGE, EVEN IF
A LIMITED REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED OF ITS
ESSENTIAL PURPOSE. YOU SPECIFICALLY ACKNOWLEDGE THAT HABITRPG IS NOT
LIABLE FOR THE DEFAMATORY, OFFENSIVE OR ILLEGAL CONDUCT OF OTHER USERS
OR THIRD PARTIES AND THAT THE RISK OF INJURY FROM THE FOREGOING RESTS
ENTIRELY WITH YOU. FURTHER, HABITRPG WILL HAVE NO LIABILITY TO YOU OR TO
ANY THIRD PARTY FOR ANY PUBLIC USER CONTENT OR THIRD-PARTY CONTENT UPLOADED
ONTO OR DOWNLOADED FROM THE SITES OR THROUGH THE SERVICES.
</p>
<p>
IN NO EVENT WILL HABITRPG'S AGGREGATE LIABILITY ARISING OUT OF OR
IN CONNECTION WITH THESE TERMS OF SERVICE OR FROM THE USE OF OR
INABILITY TO USE THE SITE, SERVICES OR CONTENT THEREIN EXCEED ONE
HUNDRED U.S. DOLLARS ($100). THE LIMITATIONS OF DAMAGES SET FORTH ABOVE
ARE FUNDAMENTAL ELEMENTS OF THE BASIS OF THE BARGAIN BETWEEN HABITRPG AND
YOU. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF
LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, SO THE ABOVE
LIMITATION MAY NOT APPLY TO YOU.
</p>
<p>
<strong>Proprietary Rights Notices</strong>
<br>
All trademarks, service marks, logos, trade names and any other
proprietary designations of HabitRPG used herein are trademarks or
registered trademarks of HabitRPG. Any other trademarks, service marks,
logos, trade names and any other proprietary designations are the
trademarks or registered trademarks of their respective parties.
</p>
<p>
<strong>Controlling Law and Jurisdiction</strong>
<br>
These Terms of Service and any action related thereto will be governed
by the laws of the State of California without regard to its conflict of
laws provisions. The exclusive jurisdiction and venue of any action with
respect to the subject matter of these Terms of Service will be the
courts having jurisdiction over disputes arising in Santa Clara County,
California, and each of the parties hereto waives any objection to
jurisdiction and venue in such courts.
</p>
<p>
YOU AGREE THAT IF YOU WANT TO SUE US, YOU MUST FILE YOUR LAWSUIT
WITHIN ONE YEAR AFTER THE EVENT THAT GAVE RISE TO YOUR LAWSUIT.
OTHERWISE, YOUR LAWSUIT WILL BE PERMANENTLY BARRED.
</p>
<p>
<strong>Export Control</strong>
<br>
You may not use or otherwise export or re-export the Services except as
authorized by United States law and the laws of the jurisdiction in
which the Services were obtained. In particular, but without limitation,
the Services may not be exported or re-exported (a) into any U.S.
embargoed countries or (b) to anyone on the U.S. Treasury Department's
list of Specially Designated Nationals or the U.S. Department of
Commerce Denied Person's List or Entity List. By using the Services, you
represent and warrant that you are not located in any such country or on
any such list. You also agree that you will not use these products for
any purposes prohibited by United States law, including, without
limitation, the development, design, manufacture or production of
nuclear, missiles, or chemical or biological weapons.
</p>
<p>
<strong>Entire Agreement</strong>
<br>
These Terms of Service constitute the entire and exclusive understanding
and agreement between HabitRPG and you regarding the Services and HabitRPG
Content, and these Terms of Service supersede and replace any and all
prior oral or written understandings or agreements between HabitRPG and
you regarding the Services and HabitRPG Content.
</p>
<p>
<strong>Assignment</strong>
<br>
You may not assign or transfer these Terms of Service, by operation of
law or otherwise, without HabitRPG's prior written consent. Any attempt
by you to assign or transfer these Terms of Service, without such
consent, will be null and of no effect. HabitRPG may freely assign these
Terms of Service. Subject to the foregoing, these Terms of Service will
bind and inure to the benefit of the parties, their successors and
permitted assigns.
</p>
<p>
<strong>Notices</strong>
<br>
You consent to the use of: (i) electronic means to complete these Terms
of Service and to deliver any notices or other communications permitted
or required hereunder; and (ii) electronic records to store information
related to these Terms of Service or your use of the Services. Any
notices or other communications permitted to required hereunder,
including those regarding modifications to these Terms of Service, will
be in writing and given: (x) by HabitRPG via email (in each case to the
address that you provide) or (y) by posting to the Sites or Services.
For notices made by e-mail, the date of receipt will be deemed the date
on which such notice is transmitted.
</p>
<p>
<strong>General</strong>
<br>
The failure of HabitRPG to enforce any right or provision of these Terms
of Service will not constitute a waiver of future enforcement of that
right or provision. The waiver of any such right or provision will be
effective only if in writing and signed by a duly authorized
representative of HabitRPG. Except as expressly set forth in these Terms
of Service, the exercise by either party of any of its remedies under
these Terms of Service will be without prejudice to its other remedies
under these Terms of Service or otherwise. If for any reason a court of
competent jurisdiction finds any provision of these Terms of Service
invalid or unenforceable, that provision will be enforced to the maximum
extent permissible and the other provisions of these Terms of Service
will remain in full force and effect.
</p>
<p>
<strong>Contacting Us</strong>
<br>
If you have any questions about these Terms of Service, please contact
us at tylerrenelle@gmail.com.
</p>
<div id="home_static_footer">
© 2012 OCDevel
</div>
</div>