start moving scoring functions from module.exports to user.fns. Will want to clean this up & optimize

This commit is contained in:
Tyler Renelle
2013-12-13 15:11:04 -07:00
parent cb20776858
commit 15826f3722
2 changed files with 668 additions and 669 deletions
+345 -363
View File
@@ -10031,7 +10031,7 @@ var global=self;/**
},{"lodash":3}],6:[function(require,module,exports){
var process=require("__browserify_process");(function() {
var HP, XP, api, content, dayMapping, moment, preenHistory, randomDrop, randomVal, sanitizeOptions, updateStats, _,
var HP, XP, api, content, dayMapping, moment, preenHistory, randomVal, sanitizeOptions, _,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
moment = require('moment');
@@ -10171,64 +10171,6 @@ var process=require("__browserify_process");(function() {
return result;
};
randomDrop = function(user, modifiers) {
var a, acceptableDrops, alpha, chanceMultiplier, delta, drop, max, priority, rarity, reachedDropLimit, streak, _base, _base1, _base2, _base3, _name, _name1, _name2, _ref;
delta = modifiers.delta, priority = modifiers.priority, streak = modifiers.streak;
if (streak == null) {
streak = 0;
}
if ((_base = user.items).lastDrop == null) {
_base.lastDrop = {
date: +moment().subtract('d', 1),
count: 0
};
}
reachedDropLimit = (api.daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= 5);
if (reachedDropLimit) {
return;
}
chanceMultiplier = Math.abs(delta);
chanceMultiplier *= priority;
chanceMultiplier += streak;
max = 0.75;
a = 0.1;
alpha = a * max * chanceMultiplier / (a * chanceMultiplier + max);
if (((_ref = user.flags) != null ? _ref.dropsEnabled : void 0) && Math.random() < alpha) {
rarity = Math.random();
if (rarity > .6) {
drop = randomVal(_.omit(content.food, 'Saddle'));
if ((_base1 = user.items.food)[_name = drop.name] == null) {
_base1[_name] = 0;
}
user.items.food[drop.name] += 1;
drop.type = 'Food';
drop.dialog = "You've found a " + drop.text + " Food! " + drop.notes;
} else if (rarity > .3) {
drop = randomVal(content.eggs);
if ((_base2 = user.items.eggs)[_name1 = drop.name] == null) {
_base2[_name1] = 0;
}
user.items.eggs[drop.name]++;
drop.type = 'Egg';
drop.dialog = "You've found a " + drop.text + " Egg! " + drop.notes;
} else {
acceptableDrops = rarity < .03 ? ['Golden'] : rarity < .09 ? ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'] : rarity < .18 ? ['Red', 'Shade', 'Skeleton'] : ['Base', 'White', 'Desert'];
drop = randomVal(_.pick(content.hatchingPotions, (function(v, k) {
return __indexOf.call(acceptableDrops, k) >= 0;
})));
if ((_base3 = user.items.hatchingPotions)[_name2 = drop.name] == null) {
_base3[_name2] = 0;
}
user.items.hatchingPotions[drop.name]++;
drop.type = 'HatchingPotion';
drop.dialog = "You've found a " + drop.text + " Hatching Potion! " + drop.notes;
}
user._tmp.drop = drop;
user.items.lastDrop.date = +(new Date);
return user.items.lastDrop.count++;
}
};
/*
------------------------------------------------------
Scoring
@@ -10236,282 +10178,12 @@ var process=require("__browserify_process");(function() {
*/
api.tnl = function(level) {
var value;
if (level >= 100) {
value = 0;
api.tnl = function(lvl) {
if (lvl >= 100) {
return 0;
} else {
value = Math.round(((Math.pow(level, 2) * 0.25) + (10 * level) + 139.75) / 10) * 10;
return Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 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
*/
api.expModifier = function(value, weaponStr, level, priority) {
var exp, str, strMod, totalStr;
if (priority == null) {
priority = 1;
}
str = (level - 1) / 2;
totalStr = (str + weaponStr) / 100;
strMod = 1 + totalStr;
exp = value * XP * strMod * 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
*/
api.hpModifier = function(value, armorDef, helmDef, shieldDef, level, priority) {
var def, defMod, hp, totalDef;
if (priority == null) {
priority = 1;
}
def = (level - 1) / 2;
totalDef = (def + armorDef + helmDef + shieldDef) / 100;
defMod = 1 - totalDef;
hp = value * HP * defMod * priority;
return Math.round(hp * 10) / 10;
};
/*
Future use
{priority} user-defined priority multiplier
*/
api.gpModifier = function(value, modifier, priority, streak, user) {
var afterStreak, streakBonus, val;
if (priority == null) {
priority = 1;
}
val = value * modifier * priority;
if (streak && user) {
streakBonus = streak / 100 + 1;
afterStreak = val * streakBonus;
if (val > 0) {
user._tmp.streakBonus = afterStreak - val;
}
return afterStreak;
} else {
return val;
}
};
/*
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
*/
api.taskDeltaFormula = function(currentValue, direction) {
var delta;
if (currentValue < -47.27) {
currentValue = -47.27;
} else if (currentValue > 21.27) {
currentValue = 21.27;
}
delta = Math.pow(0.9747, currentValue);
if (direction === 'up') {
return delta;
}
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 = function(user, newStats) {
var gp, tnl;
if (user.stats.hp <= 0) {
return;
}
if (newStats.hp != null) {
if (newStats.hp <= 0) {
user.stats.hp = 0;
return;
} else {
user.stats.hp = newStats.hp;
}
}
if (newStats.exp != null) {
tnl = api.tnl(user.stats.lvl);
if (user.stats.lvl >= 100) {
newStats.gp += newStats.exp / 15;
newStats.exp = 0;
user.stats.lvl = 100;
} else {
if (newStats.exp >= tnl) {
user.stats.exp = newStats.exp;
while (newStats.exp >= tnl && user.stats.lvl < 100) {
newStats.exp -= tnl;
user.stats.lvl++;
tnl = api.tnl(user.stats.lvl);
}
if (user.stats.lvl === 100) {
newStats.exp = 0;
}
user.stats.hp = 50;
}
}
user.stats.exp = newStats.exp;
if (user.flags == null) {
user.flags = {};
}
if (!user.flags.customizationsNotification && (user.stats.exp > 10 || user.stats.lvl > 1)) {
user.flags.customizationsNotification = true;
}
if (!user.flags.itemsEnabled && user.stats.lvl >= 2) {
user.flags.itemsEnabled = true;
}
if (!user.flags.partyEnabled && user.stats.lvl >= 3) {
user.flags.partyEnabled = true;
}
if (!user.flags.dropsEnabled && user.stats.lvl >= 4) {
user.flags.dropsEnabled = true;
user.items.eggs["Wolf"] = 1;
}
if (!user.flags.classSelected && user.stats.lvl >= 5) {
user.flags.classSelected;
}
}
if (newStats.gp != null) {
if ((typeof gp === "undefined" || gp === null) || gp < 0) {
gp = 0.0;
}
return user.stats.gp = newStats.gp;
}
};
/*
------------------------------------------------------
Cron
------------------------------------------------------
*/
/*
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
Make sure to run this function once in a while as server will not take care of overnight calculations.
And you have to run it every time client connects.
{user}
*/
api.cron = function(user, options) {
var daysMissed, expTally, lvl, now, todoTally, _base, _base1;
if (options == null) {
options = {};
}
now = [+options.now || +(new Date)][0];
daysMissed = api.daysSince(user.lastCron, _.defaults({
now: now
}, user.preferences));
if (!(daysMissed > 0)) {
return;
}
user.auth.timestamps.loggedin = new Date();
user.lastCron = now;
if (user.items.lastDrop.count > 0) {
user.items.lastDrop.count = 0;
}
if (user.preferences.sleep === true) {
return;
}
todoTally = 0;
user.todos.concat(user.dailys).forEach(function(task) {
var absVal, completed, id, repeat, scheduleMisses, type;
if (!task) {
return;
}
if (user.stats.buffs.stealth && user.stats.buffs.stealth--) {
return;
}
id = task.id, type = task.type, completed = task.completed, repeat = task.repeat;
if (!completed) {
scheduleMisses = daysMissed;
if ((type === 'daily') && repeat) {
scheduleMisses = 0;
_.times(daysMissed, function(n) {
var thatDay;
thatDay = moment(now).subtract('days', n + 1);
if (api.shouldDo(thatDay, repeat, user.preferences)) {
return scheduleMisses++;
}
});
}
if (scheduleMisses > 0) {
user.ops.score({
params: {
id: task.id,
direction: 'down'
},
query: {
times: scheduleMisses,
cron: true
}
});
}
}
switch (type) {
case 'daily':
(task.history != null ? task.history : task.history = []).push({
date: +(new Date),
value: task.value
});
return task.completed = false;
case 'todo':
absVal = completed ? Math.abs(task.value) : task.value;
return todoTally += absVal;
}
});
user.habits.forEach(function(task) {
if (task.up === false || task.down === false) {
if (Math.abs(task.value) < 0.1) {
return task.value = 0;
} else {
return task.value = task.value / 2;
}
}
});
((_base = (user.history != null ? user.history : user.history = {})).todos != null ? (_base = (user.history != null ? user.history : user.history = {})).todos : _base.todos = []).push({
date: now,
value: todoTally
});
expTally = user.stats.exp;
lvl = 0;
while (lvl < (user.stats.lvl - 1)) {
lvl++;
expTally += api.tnl(lvl);
}
((_base1 = user.history).exp != null ? (_base1 = user.history).exp : _base1.exp = []).push({
date: now,
value: expTally
});
api.preenUserHistory(user);
return user;
};
/*
@@ -10556,29 +10228,6 @@ var process=require("__browserify_process");(function() {
return newHistory;
};
api.preenUserHistory = function(user, minHistLen) {
if (minHistLen == null) {
minHistLen = 7;
}
_.each(user.habits.concat(user.dailys), function(task) {
var _ref;
if (((_ref = task.history) != null ? _ref.length : void 0) > minHistLen) {
task.history = preenHistory(task.history);
}
return true;
});
_.defaults(user.history, {
todos: [],
exp: []
});
if (user.history.exp.length > minHistLen) {
user.history.exp = preenHistory(user.history.exp);
}
if (user.history.todos.length > minHistLen) {
return user.history.todos = preenHistory(user.history.todos);
}
};
/*
------------------------------------------------------
Content
@@ -10882,10 +10531,9 @@ var process=require("__browserify_process");(function() {
user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to
move to $resource for user
* Move to $resource!
* Migrate the remaining parts: Tags, feeding pets, etc
* Migrate the remaining parts: Tags, party invitiations
BUGS
* User registration doesn't refresh, and you have to click "Play" again (and refresh *yet again* after getting in)
* Daily repeats don't have the days showing up (translations issue?)
*/
@@ -11212,7 +10860,7 @@ var process=require("__browserify_process");(function() {
}
return _.times(times, function() {
var nextDelta;
nextDelta = api.taskDeltaFormula(value, direction);
nextDelta = user.fns.taskDeltaFormula(value, direction);
if (adjustvalue) {
value += nextDelta;
}
@@ -11222,11 +10870,11 @@ var process=require("__browserify_process");(function() {
addPoints = function() {
var weaponStr;
weaponStr = user.fns.getItem('weapon').str;
exp += api.expModifier(delta, weaponStr, user.stats.lvl, priority) / 2;
exp += user.fns.expModifier(delta, weaponStr, user.stats.lvl, priority) / 2;
if (streak) {
return gp += api.gpModifier(delta, 1, priority, streak, user);
return gp += user.fns.gpModifier(delta, 1, priority, streak);
} else {
return gp += api.gpModifier(delta, 1, priority);
return gp += user.fns.gpModifier(delta, 1, priority);
}
};
subtractPoints = function() {
@@ -11234,7 +10882,7 @@ var process=require("__browserify_process");(function() {
armorDef = user.fns.getItem('armor').con;
headDef = user.fns.getItem('head').con;
shieldDef = user.fns.getItem('shield').con;
return hp += api.hpModifier(delta, armorDef, headDef, shieldDef, user.stats.lvl, priority);
return hp += user.fns.hpModifier(delta, armorDef, headDef, shieldDef, user.stats.lvl, priority);
};
switch (type) {
case 'habit':
@@ -11380,6 +11028,340 @@ var process=require("__browserify_process");(function() {
return _.reduce(path.split('.'), (function(curr, next) {
return curr != null ? curr[next] : void 0;
}), user);
},
randomDrop: function(modifiers) {
var a, acceptableDrops, alpha, chanceMultiplier, delta, drop, max, priority, rarity, reachedDropLimit, streak, _base, _base1, _base2, _base3, _name, _name1, _name2, _ref;
delta = modifiers.delta, priority = modifiers.priority, streak = modifiers.streak;
if (streak == null) {
streak = 0;
}
if ((_base = user.items).lastDrop == null) {
_base.lastDrop = {
date: +moment().subtract('d', 1),
count: 0
};
}
reachedDropLimit = (api.daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= 5);
if (reachedDropLimit) {
return;
}
chanceMultiplier = Math.abs(delta);
chanceMultiplier *= priority;
chanceMultiplier += streak;
max = 0.75;
a = 0.1;
alpha = a * max * chanceMultiplier / (a * chanceMultiplier + max);
if (((_ref = user.flags) != null ? _ref.dropsEnabled : void 0) && Math.random() < alpha) {
rarity = Math.random();
if (rarity > .6) {
drop = randomVal(_.omit(content.food, 'Saddle'));
if ((_base1 = user.items.food)[_name = drop.name] == null) {
_base1[_name] = 0;
}
user.items.food[drop.name] += 1;
drop.type = 'Food';
drop.dialog = "You've found a " + drop.text + " Food! " + drop.notes;
} else if (rarity > .3) {
drop = randomVal(content.eggs);
if ((_base2 = user.items.eggs)[_name1 = drop.name] == null) {
_base2[_name1] = 0;
}
user.items.eggs[drop.name]++;
drop.type = 'Egg';
drop.dialog = "You've found a " + drop.text + " Egg! " + drop.notes;
} else {
acceptableDrops = rarity < .03 ? ['Golden'] : rarity < .09 ? ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'] : rarity < .18 ? ['Red', 'Shade', 'Skeleton'] : ['Base', 'White', 'Desert'];
drop = randomVal(_.pick(content.hatchingPotions, (function(v, k) {
return __indexOf.call(acceptableDrops, k) >= 0;
})));
if ((_base3 = user.items.hatchingPotions)[_name2 = drop.name] == null) {
_base3[_name2] = 0;
}
user.items.hatchingPotions[drop.name]++;
drop.type = 'HatchingPotion';
drop.dialog = "You've found a " + drop.text + " Hatching Potion! " + drop.notes;
}
user._tmp.drop = drop;
user.items.lastDrop.date = +(new Date);
return user.items.lastDrop.count++;
}
},
/*
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
*/
expModifier: function(value, weaponStr, level, priority) {
var exp, str, strMod, totalStr;
if (priority == null) {
priority = 1;
}
str = (level - 1) / 2;
totalStr = (str + weaponStr) / 100;
strMod = 1 + totalStr;
exp = value * XP * strMod * 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
*/
hpModifier: function(value, armorDef, helmDef, shieldDef, level, priority) {
var def, defMod, hp, totalDef;
if (priority == null) {
priority = 1;
}
def = (level - 1) / 2;
totalDef = (def + armorDef + helmDef + shieldDef) / 100;
defMod = 1 - totalDef;
hp = value * HP * defMod * priority;
return Math.round(hp * 10) / 10;
},
/*
Future use
{priority} user-defined priority multiplier
*/
gpModifier: function(value, modifier, priority, streak) {
var afterStreak, streakBonus, val;
if (priority == null) {
priority = 1;
}
val = value * modifier * priority;
if (streak && user) {
streakBonus = streak / 100 + 1;
afterStreak = val * streakBonus;
if (val > 0) {
user._tmp.streakBonus = afterStreak - val;
}
return afterStreak;
} else {
return val;
}
},
/*
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
*/
taskDeltaFormula: function(currentValue, direction) {
var delta;
if (currentValue < -47.27) {
currentValue = -47.27;
} else if (currentValue > 21.27) {
currentValue = 21.27;
}
delta = Math.pow(0.9747, currentValue);
if (direction === 'up') {
return delta;
}
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: function(newStats) {
var gp, tnl;
if (user.stats.hp <= 0) {
return;
}
if (newStats.hp != null) {
if (newStats.hp <= 0) {
user.stats.hp = 0;
return;
} else {
user.stats.hp = newStats.hp;
}
}
if (newStats.exp != null) {
tnl = api.tnl(user.stats.lvl);
if (user.stats.lvl >= 100) {
newStats.gp += newStats.exp / 15;
newStats.exp = 0;
user.stats.lvl = 100;
} else {
if (newStats.exp >= tnl) {
user.stats.exp = newStats.exp;
while (newStats.exp >= tnl && user.stats.lvl < 100) {
newStats.exp -= tnl;
user.stats.lvl++;
tnl = api.tnl(user.stats.lvl);
}
if (user.stats.lvl === 100) {
newStats.exp = 0;
}
user.stats.hp = 50;
}
}
user.stats.exp = newStats.exp;
if (user.flags == null) {
user.flags = {};
}
if (!user.flags.customizationsNotification && (user.stats.exp > 10 || user.stats.lvl > 1)) {
user.flags.customizationsNotification = true;
}
if (!user.flags.itemsEnabled && user.stats.lvl >= 2) {
user.flags.itemsEnabled = true;
}
if (!user.flags.partyEnabled && user.stats.lvl >= 3) {
user.flags.partyEnabled = true;
}
if (!user.flags.dropsEnabled && user.stats.lvl >= 4) {
user.flags.dropsEnabled = true;
user.items.eggs["Wolf"] = 1;
}
if (!user.flags.classSelected && user.stats.lvl >= 5) {
user.flags.classSelected;
}
}
if (newStats.gp != null) {
if ((typeof gp === "undefined" || gp === null) || gp < 0) {
gp = 0.0;
}
return user.stats.gp = newStats.gp;
}
},
/*
------------------------------------------------------
Cron
------------------------------------------------------
*/
/*
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
Make sure to run this function once in a while as server will not take care of overnight calculations.
And you have to run it every time client connects.
{user}
*/
cron: function(options) {
var daysMissed, expTally, lvl, now, todoTally, _base, _base1;
if (options == null) {
options = {};
}
now = [+options.now || +(new Date)][0];
daysMissed = api.daysSince(user.lastCron, _.defaults({
now: now
}, user.preferences));
if (!(daysMissed > 0)) {
return;
}
user.auth.timestamps.loggedin = new Date();
user.lastCron = now;
if (user.items.lastDrop.count > 0) {
user.items.lastDrop.count = 0;
}
if (user.preferences.sleep === true) {
return;
}
todoTally = 0;
user.todos.concat(user.dailys).forEach(function(task) {
var absVal, completed, id, repeat, scheduleMisses, type;
if (!task) {
return;
}
if (user.stats.buffs.stealth && user.stats.buffs.stealth--) {
return;
}
id = task.id, type = task.type, completed = task.completed, repeat = task.repeat;
if (!completed) {
scheduleMisses = daysMissed;
if ((type === 'daily') && repeat) {
scheduleMisses = 0;
_.times(daysMissed, function(n) {
var thatDay;
thatDay = moment(now).subtract('days', n + 1);
if (api.shouldDo(thatDay, repeat, user.preferences)) {
return scheduleMisses++;
}
});
}
if (scheduleMisses > 0) {
user.ops.score({
params: {
id: task.id,
direction: 'down'
},
query: {
times: scheduleMisses,
cron: true
}
});
}
}
switch (type) {
case 'daily':
(task.history != null ? task.history : task.history = []).push({
date: +(new Date),
value: task.value
});
return task.completed = false;
case 'todo':
absVal = completed ? Math.abs(task.value) : task.value;
return todoTally += absVal;
}
});
user.habits.forEach(function(task) {
if (task.up === false || task.down === false) {
if (Math.abs(task.value) < 0.1) {
return task.value = 0;
} else {
return task.value = task.value / 2;
}
}
});
((_base = (user.history != null ? user.history : user.history = {})).todos != null ? (_base = (user.history != null ? user.history : user.history = {})).todos : _base.todos = []).push({
date: now,
value: todoTally
});
expTally = user.stats.exp;
lvl = 0;
while (lvl < (user.stats.lvl - 1)) {
lvl++;
expTally += api.tnl(lvl);
}
((_base1 = user.history).exp != null ? (_base1 = user.history).exp : _base1.exp = []).push({
date: now,
value: expTally
});
user.fns.preenUserHistory();
return user;
},
preenUserHistory: function(minHistLen) {
if (minHistLen == null) {
minHistLen = 7;
}
_.each(user.habits.concat(user.dailys), function(task) {
var _ref;
if (((_ref = task.history) != null ? _ref.length : void 0) > minHistLen) {
task.history = preenHistory(task.history);
}
return true;
});
_.defaults(user.history, {
todos: [],
exp: []
});
if (user.history.exp.length > minHistLen) {
user.history.exp = preenHistory(user.history.exp);
}
if (user.history.todos.length > minHistLen) {
return user.history.todos = preenHistory(user.history.todos);
}
}
};
Object.defineProperty(user, '_statsComputed', {
+323 -306
View File
@@ -75,78 +75,7 @@ randomVal = (obj) ->
result = val if Math.random() < (1 / ++count)
result
randomDrop = (user, modifiers) ->
{delta, priority, streak} = modifiers
streak ?= 0
# limit drops to 2 / day
user.items.lastDrop ?=
date: +moment().subtract('d', 1) # trick - set it to yesterday on first run, that way they can get drops today
count: 0
reachedDropLimit = (api.daysSince(user.items.lastDrop.date, user.preferences) is 0) and (user.items.lastDrop.count >= 5)
return if reachedDropLimit
# % chance of getting a pet or meat
chanceMultiplier = Math.abs(delta)
chanceMultiplier *= priority # multiply chance by reddness
chanceMultiplier += streak # streak bonus
# Temporary solution to lower the maximum drop chance to 75 percent. More thorough
# overhaul of drop changes is needed. See HabitRPG/habitrpg#1922 for details.
# Old drop chance:
# if user.flags?.dropsEnabled and Math.random() < (.05 * chanceMultiplier)
max = 0.75 # Max probability of drop
a = 0.1 # rate of increase
alpha = a*max*chanceMultiplier/(a*chanceMultiplier+max) # current probability of drop
if user.flags?.dropsEnabled and Math.random() < alpha
# current breakdown - 1% (adjustable) chance on drop
# If they got a drop: 50% chance of egg, 50% Hatching Potion. If hatchingPotion, broken down further even further
rarity = Math.random()
# Food: 40% chance
if rarity > .6
drop = randomVal _.omit(content.food, 'Saddle')
user.items.food[drop.name] ?= 0
user.items.food[drop.name]+= 1
drop.type = 'Food'
drop.dialog = "You've found a #{drop.text} Food! #{drop.notes}"
# Eggs: 30% chance
else if rarity > .3
drop = randomVal content.eggs
user.items.eggs[drop.name] ?= 0
user.items.eggs[drop.name]++
drop.type = 'Egg'
drop.dialog = "You've found a #{drop.text} Egg! #{drop.notes}"
# Hatching Potion, 30% chance - break down by rarity.
else
acceptableDrops =
# Very Rare: 10% (of 30%)
if rarity < .03 then ['Golden']
# Rare: 20% (of 30%)
else if rarity < .09 then ['Zombie', 'CottonCandyPink', 'CottonCandyBlue']
# Uncommon: 30% (of 30%)
else if rarity < .18 then ['Red', 'Shade', 'Skeleton']
# Common: 40% (of 30%)
else ['Base', 'White', 'Desert']
# No Rarity (@see https://github.com/HabitRPG/habitrpg/issues/1048, we may want to remove rareness when we add mounts)
#drop = helpers.randomVal hatchingPotions
drop = randomVal _.pick(content.hatchingPotions, ((v,k) -> k in acceptableDrops))
user.items.hatchingPotions[drop.name] ?= 0
user.items.hatchingPotions[drop.name]++
drop.type = 'HatchingPotion'
drop.dialog = "You've found a #{drop.text} Hatching Potion! #{drop.notes}"
# if they've dropped something, we want the consuming client to know so they can notify the user. See how the Derby
# app handles it for example. Would this be better handled as an emit() ?
user._tmp.drop = drop
user.items.lastDrop.date = +new Date
user.items.lastDrop.count++
###
------------------------------------------------------
@@ -154,222 +83,12 @@ randomDrop = (user, modifiers) ->
------------------------------------------------------
###
api.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
###
api.expModifier = (value, weaponStr, level, priority=1) ->
str = (level - 1) / 2
# ultimately get this from user
totalStr = (str + weaponStr) / 100
strMod = 1 + totalStr
exp = value * XP * strMod * 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
###
api.hpModifier = (value, armorDef, helmDef, shieldDef, level, priority=1) ->
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 * priority
return Math.round(hp * 10) / 10
# round to 1dp
###
Future use
{priority} user-defined priority multiplier
###
api.gpModifier = (value, modifier, priority=1, streak, user) ->
val = value * modifier * priority
if streak and user
streakBonus = streak / 100 + 1 # eg, 1-day streak is 1.1, 2-day is 1.2, etc
afterStreak = val * streakBonus
user._tmp.streakBonus = afterStreak - val if (val > 0) # keep this on-hand for later, so we can notify streak-bonus
return afterStreak
else
return val
###
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
###
api.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
###
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 = (user, newStats) ->
# if user is dead, dont do anything
return if user.stats.hp <= 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
user.stats.hp = 0
return
else
user.stats.hp = newStats.hp
if newStats.exp?
tnl = api.tnl(user.stats.lvl)
#silent = false
# if we're at level 100, turn xp to gold
if user.stats.lvl >= 100
newStats.gp += newStats.exp / 15
newStats.exp = 0
user.stats.lvl = 100
else
# level up & carry-over exp
if newStats.exp >= tnl
#silent = true # push through the negative xp silently
user.stats.exp = newStats.exp # push normal + notification
while newStats.exp >= tnl and user.stats.lvl < 100 # keep levelling up
newStats.exp -= tnl
user.stats.lvl++
tnl = api.tnl(user.stats.lvl)
if user.stats.lvl == 100
newStats.exp = 0
user.stats.hp = 50
user.stats.exp = newStats.exp
# Set flags when they unlock features
user.flags ?= {}
if !user.flags.customizationsNotification and (user.stats.exp > 10 or user.stats.lvl > 1)
user.flags.customizationsNotification = true
if !user.flags.itemsEnabled and user.stats.lvl >= 2
user.flags.itemsEnabled = true
if !user.flags.partyEnabled and user.stats.lvl >= 3
user.flags.partyEnabled = true
if !user.flags.dropsEnabled and user.stats.lvl >= 4
user.flags.dropsEnabled = true
user.items.eggs["Wolf"] = 1
if !user.flags.classSelected and user.stats.lvl >= 5
user.flags.classSelected
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)
user.stats.gp = newStats.gp
api.tnl = (lvl) ->
if lvl >= 100 then 0
else Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 10
# round to nearest 10?
###
------------------------------------------------------
Cron
------------------------------------------------------
###
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
Make sure to run this function once in a while as server will not take care of overnight calculations.
And you have to run it every time client connects.
{user}
###
api.cron = (user, options={}) ->
[now] = [+options.now || +new Date]
# They went to a different timezone
# FIXME:
# (1) This exit-early code isn't taking timezone into consideration!!
# (2) Won't switching timezones be handled automatically client-side anyway? (aka, can we just remove this code?)
# (3) And if not, is this the correct way to handle switching timezones
# if moment(user.lastCron).isAfter(now)
# user.lastCron = now
# return
daysMissed = api.daysSince user.lastCron, _.defaults({now}, user.preferences)
return unless daysMissed > 0
user.auth.timestamps.loggedin = new Date()
user.lastCron = now
# Reset the lastDrop count to zero
if user.items.lastDrop.count > 0
user.items.lastDrop.count = 0
# User is resting at the inn. Used to be we un-checked each daily without performing calculation (see commits before fb29e35)
# but to prevent abusing the inn (http://goo.gl/GDb9x) we now do *not* calculate dailies, and simply set lastCron to today
return if user.preferences.sleep is true
# Tally each task
todoTally = 0
user.todos.concat(user.dailys).forEach (task) ->
return unless task
return if user.stats.buffs.stealth && user.stats.buffs.stealth-- # User "evades" a certain number of tasks
{id, type, completed, repeat} = task
# Deduct experience for missed Daily tasks, but not for Todos (just increase todo's value)
unless completed
scheduleMisses = daysMissed
# for dailys which have repeat dates, need to calculate how many they've missed according to their own schedule
if (type is 'daily') and repeat
scheduleMisses = 0
_.times daysMissed, (n) ->
thatDay = moment(now).subtract('days', n + 1)
scheduleMisses++ if api.shouldDo(thatDay, repeat, user.preferences)
if scheduleMisses > 0
user.ops.score({params:{id:task.id, direction:'down'}, query:{times:scheduleMisses, cron:true}})
switch type
when 'daily'
(task.history ?= []).push({ date: +new Date, value: task.value })
task.completed = false
when 'todo'
#get updated value
absVal = if (completed) then Math.abs(task.value) else task.value
todoTally += absVal
user.habits.forEach (task) -> # slowly reset 'onlies' value to 0
if task.up is false or task.down is false
if Math.abs(task.value) < 0.1
task.value = 0
else
task.value = task.value / 2
# Finished tallying
((user.history ?= {}).todos ?= []).push { date: now, value: todoTally }
# tally experience
expTally = user.stats.exp
lvl = 0 #iterator
while lvl < (user.stats.lvl - 1)
lvl++
expTally += api.tnl(lvl)
(user.history.exp ?= []).push { date: now, value: expTally }
api.preenUserHistory(user)
user
###
Preen history for users with > 7 history entries
@@ -407,20 +126,6 @@ preenHistory = (history) ->
newHistory
# Registered users with some history
api.preenUserHistory = (user, minHistLen = 7) ->
_.each user.habits.concat(user.dailys), (task) ->
task.history = preenHistory(task.history) if task.history?.length > minHistLen
true
_.defaults user.history, {todos:[], exp: []}
user.history.exp = preenHistory(user.history.exp) if user.history.exp.length > minHistLen
user.history.todos = preenHistory(user.history.todos) if user.history.todos.length > minHistLen
#user.markModified('history')
#user.markModified('habits')
#user.markModified('dailys')
###
------------------------------------------------------
Content
@@ -612,16 +317,19 @@ TODO
user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to
move to $resource for user
* Move to $resource!
* Migrate the remaining parts: Tags, feeding pets, etc
* Migrate the remaining parts: Tags, party invitiations
BUGS
* User registration doesn't refresh, and you have to click "Play" again (and refresh *yet again* after getting in)
* Daily repeats don't have the days showing up (translations issue?)
###
api.wrap = (user) ->
return if user._wrapped
user._wrapped = true
# ----------------------------------------------------------------------
# user.ops shared client/server operations
# ----------------------------------------------------------------------
user.ops =
sleep: (req, cb) ->
@@ -843,23 +551,23 @@ api.wrap = (user) ->
# 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 = api.taskDeltaFormula(value, direction)
nextDelta = user.fns.taskDeltaFormula(value, direction)
value += nextDelta if adjustvalue
delta += nextDelta
addPoints = ->
weaponStr = user.fns.getItem('weapon').str
exp += api.expModifier(delta, weaponStr, user.stats.lvl, priority) / 2 # /2 hack for now, people leveling too fast
exp += user.fns.expModifier(delta, weaponStr, user.stats.lvl, priority) / 2 # /2 hack for now, people leveling too fast
if streak
gp += api.gpModifier(delta, 1, priority, streak, user)
gp += user.fns.gpModifier(delta, 1, priority, streak)
else
gp += api.gpModifier(delta, 1, priority)
gp += user.fns.gpModifier(delta, 1, priority)
subtractPoints = ->
armorDef = user.fns.getItem('armor').con
headDef = user.fns.getItem('head').con
shieldDef = user.fns.getItem('shield').con
hp += api.hpModifier(delta, armorDef, headDef, shieldDef, user.stats.lvl, priority)
hp += user.fns.hpModifier(delta, armorDef, headDef, shieldDef, user.stats.lvl, priority)
switch type
when 'habit'
@@ -922,6 +630,10 @@ api.wrap = (user) ->
cb? null, req
return delta
# ----------------------------------------------------------------------
# user.fns helpers
# ----------------------------------------------------------------------
user.fns =
getItem: (type) ->
item = content.gear.flat[user.items.gear.equipped[type]]
@@ -965,6 +677,311 @@ api.wrap = (user) ->
dotGet: (path) ->
_.reduce path.split('.'), ((curr, next) => curr?[next]), user
# ----------------------------------------------------------------------
# Scoring
# ----------------------------------------------------------------------
randomDrop: (modifiers) ->
{delta, priority, streak} = modifiers
streak ?= 0
# limit drops to 2 / day
user.items.lastDrop ?=
date: +moment().subtract('d', 1) # trick - set it to yesterday on first run, that way they can get drops today
count: 0
reachedDropLimit = (api.daysSince(user.items.lastDrop.date, user.preferences) is 0) and (user.items.lastDrop.count >= 5)
return if reachedDropLimit
# % chance of getting a pet or meat
chanceMultiplier = Math.abs(delta)
chanceMultiplier *= priority # multiply chance by reddness
chanceMultiplier += streak # streak bonus
# Temporary solution to lower the maximum drop chance to 75 percent. More thorough
# overhaul of drop changes is needed. See HabitRPG/habitrpg#1922 for details.
# Old drop chance:
# if user.flags?.dropsEnabled and Math.random() < (.05 * chanceMultiplier)
max = 0.75 # Max probability of drop
a = 0.1 # rate of increase
alpha = a*max*chanceMultiplier/(a*chanceMultiplier+max) # current probability of drop
if user.flags?.dropsEnabled and Math.random() < alpha
# current breakdown - 1% (adjustable) chance on drop
# If they got a drop: 50% chance of egg, 50% Hatching Potion. If hatchingPotion, broken down further even further
rarity = Math.random()
# Food: 40% chance
if rarity > .6
drop = randomVal _.omit(content.food, 'Saddle')
user.items.food[drop.name] ?= 0
user.items.food[drop.name]+= 1
drop.type = 'Food'
drop.dialog = "You've found a #{drop.text} Food! #{drop.notes}"
# Eggs: 30% chance
else if rarity > .3
drop = randomVal content.eggs
user.items.eggs[drop.name] ?= 0
user.items.eggs[drop.name]++
drop.type = 'Egg'
drop.dialog = "You've found a #{drop.text} Egg! #{drop.notes}"
# Hatching Potion, 30% chance - break down by rarity.
else
acceptableDrops =
# Very Rare: 10% (of 30%)
if rarity < .03 then ['Golden']
# Rare: 20% (of 30%)
else if rarity < .09 then ['Zombie', 'CottonCandyPink', 'CottonCandyBlue']
# Uncommon: 30% (of 30%)
else if rarity < .18 then ['Red', 'Shade', 'Skeleton']
# Common: 40% (of 30%)
else ['Base', 'White', 'Desert']
# No Rarity (@see https://github.com/HabitRPG/habitrpg/issues/1048, we may want to remove rareness when we add mounts)
#drop = helpers.randomVal hatchingPotions
drop = randomVal _.pick(content.hatchingPotions, ((v,k) -> k in acceptableDrops))
user.items.hatchingPotions[drop.name] ?= 0
user.items.hatchingPotions[drop.name]++
drop.type = 'HatchingPotion'
drop.dialog = "You've found a #{drop.text} Hatching Potion! #{drop.notes}"
# if they've dropped something, we want the consuming client to know so they can notify the user. See how the Derby
# app handles it for example. Would this be better handled as an emit() ?
user._tmp.drop = drop
user.items.lastDrop.date = +new Date
user.items.lastDrop.count++
###
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
###
expModifier: (value, weaponStr, level, priority=1) ->
str = (level - 1) / 2
# ultimately get this from user
totalStr = (str + weaponStr) / 100
strMod = 1 + totalStr
exp = value * XP * strMod * 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
###
hpModifier: (value, armorDef, helmDef, shieldDef, level, priority=1) ->
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 * priority
return Math.round(hp * 10) / 10
# round to 1dp
###
Future use
{priority} user-defined priority multiplier
###
gpModifier: (value, modifier, priority=1, streak) ->
val = value * modifier * priority
if streak and user
streakBonus = streak / 100 + 1 # eg, 1-day streak is 1.1, 2-day is 1.2, etc
afterStreak = val * streakBonus
user._tmp.streakBonus = afterStreak - val if (val > 0) # keep this on-hand for later, so we can notify streak-bonus
return afterStreak
else
return val
###
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
###
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
###
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) ->
# if user is dead, dont do anything
return if user.stats.hp <= 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
user.stats.hp = 0
return
else
user.stats.hp = newStats.hp
if newStats.exp?
tnl = api.tnl(user.stats.lvl)
#silent = false
# if we're at level 100, turn xp to gold
if user.stats.lvl >= 100
newStats.gp += newStats.exp / 15
newStats.exp = 0
user.stats.lvl = 100
else
# level up & carry-over exp
if newStats.exp >= tnl
#silent = true # push through the negative xp silently
user.stats.exp = newStats.exp # push normal + notification
while newStats.exp >= tnl and user.stats.lvl < 100 # keep levelling up
newStats.exp -= tnl
user.stats.lvl++
tnl = api.tnl(user.stats.lvl)
if user.stats.lvl == 100
newStats.exp = 0
user.stats.hp = 50
user.stats.exp = newStats.exp
# Set flags when they unlock features
user.flags ?= {}
if !user.flags.customizationsNotification and (user.stats.exp > 10 or user.stats.lvl > 1)
user.flags.customizationsNotification = true
if !user.flags.itemsEnabled and user.stats.lvl >= 2
user.flags.itemsEnabled = true
if !user.flags.partyEnabled and user.stats.lvl >= 3
user.flags.partyEnabled = true
if !user.flags.dropsEnabled and user.stats.lvl >= 4
user.flags.dropsEnabled = true
user.items.eggs["Wolf"] = 1
if !user.flags.classSelected and user.stats.lvl >= 5
user.flags.classSelected
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)
user.stats.gp = newStats.gp
###
------------------------------------------------------
Cron
------------------------------------------------------
###
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
Make sure to run this function once in a while as server will not take care of overnight calculations.
And you have to run it every time client connects.
{user}
###
cron: (options={}) ->
[now] = [+options.now || +new Date]
# They went to a different timezone
# FIXME:
# (1) This exit-early code isn't taking timezone into consideration!!
# (2) Won't switching timezones be handled automatically client-side anyway? (aka, can we just remove this code?)
# (3) And if not, is this the correct way to handle switching timezones
# if moment(user.lastCron).isAfter(now)
# user.lastCron = now
# return
daysMissed = api.daysSince user.lastCron, _.defaults({now}, user.preferences)
return unless daysMissed > 0
user.auth.timestamps.loggedin = new Date()
user.lastCron = now
# Reset the lastDrop count to zero
if user.items.lastDrop.count > 0
user.items.lastDrop.count = 0
# User is resting at the inn. Used to be we un-checked each daily without performing calculation (see commits before fb29e35)
# but to prevent abusing the inn (http://goo.gl/GDb9x) we now do *not* calculate dailies, and simply set lastCron to today
return if user.preferences.sleep is true
# Tally each task
todoTally = 0
user.todos.concat(user.dailys).forEach (task) ->
return unless task
return if user.stats.buffs.stealth && user.stats.buffs.stealth-- # User "evades" a certain number of tasks
{id, type, completed, repeat} = task
# Deduct experience for missed Daily tasks, but not for Todos (just increase todo's value)
unless completed
scheduleMisses = daysMissed
# for dailys which have repeat dates, need to calculate how many they've missed according to their own schedule
if (type is 'daily') and repeat
scheduleMisses = 0
_.times daysMissed, (n) ->
thatDay = moment(now).subtract('days', n + 1)
scheduleMisses++ if api.shouldDo(thatDay, repeat, user.preferences)
if scheduleMisses > 0
user.ops.score({params:{id:task.id, direction:'down'}, query:{times:scheduleMisses, cron:true}})
switch type
when 'daily'
(task.history ?= []).push({ date: +new Date, value: task.value })
task.completed = false
when 'todo'
#get updated value
absVal = if (completed) then Math.abs(task.value) else task.value
todoTally += absVal
user.habits.forEach (task) -> # slowly reset 'onlies' value to 0
if task.up is false or task.down is false
if Math.abs(task.value) < 0.1
task.value = 0
else
task.value = task.value / 2
# Finished tallying
((user.history ?= {}).todos ?= []).push { date: now, value: todoTally }
# tally experience
expTally = user.stats.exp
lvl = 0 #iterator
while lvl < (user.stats.lvl - 1)
lvl++
expTally += api.tnl(lvl)
(user.history.exp ?= []).push { date: now, value: expTally }
user.fns.preenUserHistory()
user
# Registered users with some history
preenUserHistory: (minHistLen = 7) ->
_.each user.habits.concat(user.dailys), (task) ->
task.history = preenHistory(task.history) if task.history?.length > minHistLen
true
_.defaults user.history, {todos:[], exp: []}
user.history.exp = preenHistory(user.history.exp) if user.history.exp.length > minHistLen
user.history.todos = preenHistory(user.history.todos) if user.history.todos.length > minHistLen
#user.markModified('history')
#user.markModified('habits')
#user.markModified('dailys')
# ----------------------------------------------------------------------
# Virtual Attributes
# ----------------------------------------------------------------------
# Aggregate all intrinsic stats, buffs, weapon, & armor into computed stats
Object.defineProperty user, '_statsComputed',
get: ->