diff --git a/script/algos.coffee b/script/algos.coffee index e0864fd560..103e13ea6d 100644 --- a/script/algos.coffee +++ b/script/algos.coffee @@ -1,186 +1,172 @@ -({ define: ( - if typeof define == "function" - define +moment = require('moment') +_ = require('lodash') +helpers = require('./helpers') +items = require('./items') +XP = 15 +HP = 2 +obj = module.exports = {} + +obj.priorityValue = (priority = '!') -> + switch priority + when '!' then 1 + when '!!' then 1.5 + when '!!!' then 2 + else 1 + +obj.tnl = (level) -> + if level >= 100 + value = 0 else - (F)-> - F(require, exports, module) -)}).define (require, exports, module)-> - helpers = require('./helpers') - moment = require('moment') - items = require('./items') - XP = 15 - HP = 2 - obj = module.exports = - {}; - obj.priorityValue = (priority = '!') -> - switch priority - when '!' then 1 - when '!!' then 1.5 - when '!!!' then 2 - else - 1 + value = Math.round(((Math.pow(level, 2) * 0.25) + (10 * level) + 139.75) / 10) * 10 + # round to nearest 10 + return value - obj.tnl = (level) -> - if level >= 100 - value = 0 +### + 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 +### +obj.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 * obj.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 +### +obj.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 * obj.priorityValue(priority) + return Math.round(hp * 10) / 10 +# round to 1dp + +### + Future use + {priority} user-defined priority multiplier +### +obj.gpModifier = (value, modifier, priority = '!', streak, user) -> + val = value * modifier * obj.priorityValue(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 +### +obj.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 + + +### + Drop System +### + +randomDrop = (user, delta, priority, 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 = (helpers.daysBetween(user.items.lastDrop.date, +new Date) is 0) + and (user.items.lastDrop.count >= 2) + return if reachedDropLimit + + # % chance of getting a pet or meat + chanceMultiplier = Math.abs(delta) + chanceMultiplier *= obj.priorityValue(priority) # multiply chance by reddness + chanceMultiplier += streak # streak bonus + + if user.flags?.dropsEnabled and Math.random() < (.05 * chanceMultiplier) + # 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() + + # Egg, 40% chance + if rarity > .6 + drop = randomVal(pets) + (user.items.eggs ?= []).push drop + drop.type = 'Egg' + drop.dialog = "You've found a #{drop.text} Egg! #{drop.notes}" + + # Hatching Potion, 60% chance - break down by rarity even more. FIXME this may not be the best method, so revisit else - value = Math.round(((Math.pow(level, 2) * 0.25) + (10 * level) + 139.75) / 10) * 10 - # round to nearest 10 - return value + acceptableDrops = [] - ### - 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 - ### - obj.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 * obj.priorityValue(priority) - return Math.round(exp) + # Tier 5 (Blue Moon Rare) + if rarity < .1 + acceptableDrops = + ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue', + 'Golden'] - ### - 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 - ### - obj.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 * obj.priorityValue(priority) - return Math.round(hp * 10) / 10 - # round to 1dp + # Tier 4 (Very Rare) + else if rarity < .2 + acceptableDrops = + ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue'] - ### - Future use - {priority} user-defined priority multiplier - ### - obj.gpModifier = (value, modifier, priority = '!', streak, user) -> - val = value * modifier * obj.priorityValue(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.streakBonus = afterStreak - val if (val > 0) - # can we do this without model? just global emit? - return afterStreak - else - return val + # Tier 3 (Rare) + else if rarity < .3 + acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton'] - ### - 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 - ### - obj.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 - - - ### - Drop System - ### - - randomDrop = (user, delta, priority, streak = 0) -> - # limit drops to 2 / day - if !user.items.lastDrop? - user.items = - { - date: +moment().subtract('d', 1) # trick - set it to yesterday on first run, that way they can get drops today - count: 0 - } - reachedDropLimit = (helpers.daysBetween(user.items.lastDrop.date, - +new Date) is 0) and user.items.lastDrop.count >= 2 - return if reachedDropLimit - - # % chance of getting a pet or meat - chanceMultiplier = Math.abs(delta) - chanceMultiplier *= obj.priorityValue(priority) - # multiply chance by reddness - chanceMultiplier += streak - # streak bonus - console.log chanceMultiplier - - if user.flags.dropsEnabled and Math.random() < (.05 * chanceMultiplier) - # current breakdown - 3% (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() - - # Egg, 40% chance - if rarity > .6 - drop = randomVal(pets) - user.items.eggs.push drop - drop.type = 'Egg' - drop.dialog = "You've found a #{drop.text} Egg! #{drop.notes}" - - # Hatching Potion, 60% chance - break down by rarity even more. FIXME this may not be the best method, so revisit + # Tier 2 (Scarce) + else if rarity < .4 + acceptableDrops = ['Base', 'White', 'Desert'] + # Tier 1 (Common) else - acceptableDrops = [] + acceptableDrops = ['Base'] - # Tier 5 (Blue Moon Rare) - if rarity < .1 - acceptableDrops = - ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue', - 'Golden'] + acceptableDrops = hatchingPotions.filter (hatchingPotion) -> + hatchingPotion.name in acceptableDrops + drop = randomVal acceptableDrops + (user.items.hatchingPotions ?= []).push drop.name + drop.type = 'HatchingPotion' + drop.dialog = "You've found a #{drop.text} Hatching Potion! #{drop.notes}" - # Tier 4 (Very Rare) - else if rarity < .2 - acceptableDrops = - ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue'] + (user._tmp?={}).drop = drop + #FIXME $('#item-dropped-modal').modal 'show' - # Tier 3 (Rare) - else if rarity < .3 - acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton'] + user.items.lastDrop.date = +new Date + user.items.lastDrop.count++ - # Tier 2 (Scarce) - else if rarity < .4 - acceptableDrops = ['Base', 'White', 'Desert'] - # Tier 1 (Common) - else - acceptableDrops = ['Base'] +# {task} task you want to score +# {direction} 'up' or 'down' +obj.score = (user, task, direction, times=1, cron=false) -> + {gp, hp, exp, lvl} = user.stats + {type, value, streak} = task + priority = task.priority or '!' - acceptableDrops = hatchingPotions.filter (hatchingPotion) -> - hatchingPotion.name in acceptableDrops - drop = randomVal acceptableDrops - user.items.hatchingPotions.push drop.name - drop.type = 'HatchingPotion' - drop.dialog = "You've found a #{drop.text} Hatching Potion! #{drop.notes}" + # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it + if task.value > user.stats.gp and task.type is 'reward' + return unless confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." - user.drop = drop - - user.items.lastDrop.date = +new Date - user.items.lastDrop.count++ - - # {task} task you want to score - # {direction} 'up' or 'down' - obj.score = (user, task, direction) -> - {gp, hp, exp, lvl} = user.stats - {type, value, streak} = task - priority = task.priority or '!' - - # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it - if task.value > user.stats.gp and task.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 - #TODO if this rule is working OK. - return - - delta = 0 - calculateDelta = (adjustvalue = true) -> + delta = 0 + calculateDelta = (adjustvalue = true) -> + # If multiple days have passed, multiply times days missed + _.times times, -> # 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 @@ -188,194 +174,201 @@ value += nextDelta if adjustvalue delta += nextDelta - addPoints = -> - level = user.stats.lvl - weaponStrength = items.items.weapon[user.items.weapon].strength - exp += obj.expModifier(delta, weaponStrength, level, priority) / 2 - # / 2 hack for now bcause people leveling too fast - if streak - gp += obj.gpModifier(delta, 1, priority, streak, user) - else - gp += obj.gpModifier(delta, 1, priority) + addPoints = -> + level = user.stats.lvl + weaponStrength = items.items.weapon[user.items.weapon].strength + exp += obj.expModifier(delta, weaponStrength, level, priority) / 2 + # / 2 hack for now bcause people leveling too fast + if streak + gp += obj.gpModifier(delta, 1, priority, streak, user) + else + gp += obj.gpModifier(delta, 1, priority) - subtractPoints = -> - level = user.stats.lvl - armorDefense = items.items.armor[user.items.armor].defense - helmDefense = items.items.head[user.items.head].defense - shieldDefense = items.items.shield[user.items.shield].defense - hp += obj.hpModifier(delta, armorDefense, helmDefense, shieldDefense, level, priority) + subtractPoints = -> + level = user.stats.lvl + armorDefense = items.items.armor[user.items.armor].defense + helmDefense = items.items.head[user.items.head].defense + shieldDefense = items.items.shield[user.items.shield].defense + hp += obj.hpModifier(delta, armorDefense, helmDefense, shieldDefense, level, priority) - switch type - when 'habit' + switch type + when 'habit' + calculateDelta() + # Add habit value to habit-history (if different) + if (delta > 0) then addPoints() else subtractPoints() + if task.value != value + (task.history ?= []).push { date: +new Date, value: value } + + when 'daily' + if cron? # cron calculateDelta() - # Add habit value to habit-history (if different) - if (delta > 0) then addPoints() else subtractPoints() - task.history ?= [] - if task.value != value - historyEntry = { date: +new Date, value: value } - task.history.push historyEntry - - when 'daily' + subtractPoints() + task.streak = 0 + else calculateDelta(false) if delta != 0 - addPoints() - # obviously for delta>0, but also a trick to undo accidental checkboxes + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes if direction is 'up' streak = if streak then streak + 1 else 1 else streak = if streak then streak - 1 else 0 task.streak = streak - when 'todo' + 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(task.value) - num = parseFloat(task.value).toFixed(2) - # if too expensive, reduce health & zero gp - if gp < 0 - hp += gp - # hp - gp difference - gp = 0 + when 'reward' + # Don't adjust values for rewards + calculateDelta(false) + # purchase item + gp -= Math.abs(task.value) + num = parseFloat(task.value).toFixed(2) + # if too expensive, reduce health & zero gp + if gp < 0 + hp += gp + # hp - gp difference + gp = 0 - task.value = value - updateStats user, { hp, exp, gp } + task.value = value + updateStats user, { hp, exp, gp } - # Drop system - # randomDrop(user, delta, priority, streak) if direction is 'up' + # Drop system #FIXME + randomDrop(user, delta, priority, streak) if direction is '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 = (user, newStats) -> - # if user is dead, dont do anything - return if user.stats.hp <= 0 +### + 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 - # signifies dead - return - else - user.stats.hp = newStats.hp + if newStats.hp? + # Game Over + if newStats.hp <= 0 + user.stats.hp = 0 # signifies dead + return + else + user.stats.hp = newStats.hp - if newStats.exp? - tnl = obj.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 = obj.tnl(user.stats.lvl) - if user.stats.lvl == 100 - newStats.exp = 0 - user.stats.hp = 50 + if newStats.exp? + tnl = obj.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 = obj.tnl(user.stats.lvl) + if user.stats.lvl == 100 + newStats.exp = 0 + user.stats.hp = 50 - user.stats.exp = newStats.exp - #if silent - #console.log("pushing silent :" + obj.stats.exp) + user.stats.exp = newStats.exp + #if silent + #console.log("pushing silent :" + obj.stats.exp) - # Set flags when they unlock features - 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 + # Set flags when they unlock features + 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 - 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 + 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 - ### - 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. - ### - obj.cron = (user) -> - today = +new Date - daysPassed = helpers.daysBetween(user.lastCron, today, user.preferences.dayStart) - if daysPassed > 0 - user.lastCron = today +### + 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. +### +obj.cron = (user) -> + today = +new Date - if user.flags.rest is true - user.dailys.forEach (daily) -> - daily.completed = false - return + lastCron = user.lastCron + # New user (!lastCron, lastCron==new) or it got busted somehow, maybe they went to a different timezone + if !lastCron? or lastCron is 'new' or moment(lastCron).isAfter(today) + user.lastCron = +new Date + return - # Tally each task - todoTally = 0 - user.todos.concat(user.dailys).forEach (task) -> - {id, type, completed, repeat} = task - # 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 - for i in [1..daysPassed] by 1 - thatDay = moment().subtract('days', i + 1) - if repeat[helpers.dayMapping[thatDay.day()]] == true - daysFailed++ - score user, task, 'down' - if type == 'daily' + daysPassed = helpers.daysBetween(user.lastCron, today, user.preferences?.dayStart) + if daysPassed > 0 + user.lastCron = today + + # 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.flags.rest is true + + # Tally each task + todoTally = 0 + user.todos.concat(user.dailys).forEach (task) -> + {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 daysPassed, (n) -> + thatDay = moment(today).subtract('days', n + 1) + scheduleMisses++ if helpers.shouldDo(thatDay, repeat, obj.preferences?.dayStart) is true + #FIXME with times & cron + score(user, task, 'down', scheduleMisses, true) if scheduleMisses > 0 + + switch type + when 'daily' if completed #set OHV for completed dailies task.value = task.value + taskDeltaFormula(task.value, 'up') - task.history ?= [] - task.history.push { date: +new Date, value: task.value } + (task.history ?= []).push { date: +new Date, value: task.value } task.completed = false - else + + 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 == false or task.down == false - if Math.abs(task.value) < 0.1 - task.value = 0 - else - task.value = task.value / 2 + 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 ?= {}; - user.history.todos ?= []; - user.history.exp ?= [] - user.history.todos.push { date: today, value: todoTally } - # tally experience - expTally = user.stats.exp - lvl = 0 - #iterator - while lvl < (user.stats.lvl - 1) - lvl++ - expTally += obj.tnl(lvl) - user.history.exp.push { date: today, value: expTally } - user + # Finished tallying + ((user.history ?= {}).todos ?= []).push { date: today, value: todoTally } + user.history.exp ?= [] + # tally experience + expTally = user.stats.exp + lvl = 0 + #iterator + while lvl < (user.stats.lvl - 1) + lvl++ + expTally += obj.tnl(lvl) + user.history.exp.push { date: today, value: expTally } + user diff --git a/script/helpers.coffee b/script/helpers.coffee index f65afff4ff..5932858680 100644 --- a/script/helpers.coffee +++ b/script/helpers.coffee @@ -37,14 +37,16 @@ module.exports = so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where Angular sets object properties directly - in which case, this function will be used. ### - pathSet: (path, val, obj) -> + dotSet: (path, val, obj) -> arr = path.split('.') - arr.reduce (curr, next, index) -> + _.reduce arr, (curr, next, index) -> if (arr.length - 1) == index curr[next] = val curr[next] , obj + dotGet: (path, obj) -> _.reduce path.split('.'), ((curr, next) -> curr[next]), obj + daysBetween: daysBetween shouldDo: shouldDo