diff --git a/dist/habitrpg-shared.js b/dist/habitrpg-shared.js index af27a06c82..63fab329e1 100644 --- a/dist/habitrpg-shared.js +++ b/dist/habitrpg-shared.js @@ -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', { diff --git a/script/index.coffee b/script/index.coffee index 8a9eed375d..57473281f3 100644 --- a/script/index.coffee +++ b/script/index.coffee @@ -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: ->