mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-08-02 20:19:42 +00:00
classes: use predictable randomness. This allows us to run an op on both
client and server, and if randomness is involved (eg, item drops, critical hits, losing gear/stats on death) - then it will be equivalent on both client and server, while still seeming "random". This is required for the new death mechanic. Kudos to @snicker for providing the predictableRandom() function. @paglias we can probably stop using _.tmp now, and simply flag on the client when user drops an item / streak bonus. @SabreCat this will be used for crits, I'll keep you posted
This commit is contained in:
parent
8a5c46da2b
commit
2d23803207
2 changed files with 83 additions and 70 deletions
91
dist/habitrpg-shared.js
vendored
91
dist/habitrpg-shared.js
vendored
|
|
@ -10456,7 +10456,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, randomVal, sanitizeOptions, _,
|
||||
var HP, XP, api, content, dayMapping, moment, preenHistory, 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');
|
||||
|
|
@ -10569,33 +10569,6 @@ var process=require("__browserify_process");(function() {
|
|||
}
|
||||
};
|
||||
|
||||
/*
|
||||
------------------------------------------------------
|
||||
Drop System
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
Get a random property from an object
|
||||
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
|
||||
returns random property (the value)
|
||||
*/
|
||||
|
||||
|
||||
randomVal = function(obj) {
|
||||
var count, key, result, val;
|
||||
result = void 0;
|
||||
count = 0;
|
||||
for (key in obj) {
|
||||
val = obj[key];
|
||||
if (Math.random() < (1 / ++count)) {
|
||||
result = val;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
------------------------------------------------------
|
||||
Scoring
|
||||
|
|
@ -11148,7 +11121,7 @@ var process=require("__browserify_process");(function() {
|
|||
if (user.stats.lvl > 1) {
|
||||
user.stats.lvl--;
|
||||
}
|
||||
lostStat = randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
|
||||
lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
|
||||
if (user.stats[k]) {
|
||||
m[k] = k;
|
||||
}
|
||||
|
|
@ -11157,9 +11130,9 @@ var process=require("__browserify_process");(function() {
|
|||
if (lostStat) {
|
||||
user.stats[lostStat]--;
|
||||
}
|
||||
lostItem = randomVal(_.reduce(user.items.gear.owned, (function(m, v, k) {
|
||||
lostItem = user.fns.randomVal(_.reduce(user.items.gear.owned, (function(m, v, k) {
|
||||
if (v) {
|
||||
m[k] = k;
|
||||
m["" + k] = "" + k;
|
||||
}
|
||||
return m;
|
||||
}), {}));
|
||||
|
|
@ -11393,9 +11366,7 @@ var process=require("__browserify_process");(function() {
|
|||
if (direction === 'up') {
|
||||
user.fns.randomDrop({
|
||||
task: task,
|
||||
delta: delta,
|
||||
priority: task.priority,
|
||||
streak: task.streak
|
||||
delta: delta
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -11453,6 +11424,43 @@ var process=require("__browserify_process");(function() {
|
|||
});
|
||||
},
|
||||
/*
|
||||
Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
|
||||
we need things to be "random", but technically predictable so that they don't go out-of-sync
|
||||
*/
|
||||
|
||||
predictableRandom: function(seed) {
|
||||
var x;
|
||||
if (!seed || seed === Math.PI) {
|
||||
seed = _.reduce(user.stats, (function(m, v) {
|
||||
if (_.isNumber(v)) {
|
||||
return m + v;
|
||||
} else {
|
||||
return m;
|
||||
}
|
||||
}), 0);
|
||||
}
|
||||
x = Math.sin(seed++) * 10000;
|
||||
return x - Math.floor(x);
|
||||
},
|
||||
/*
|
||||
Get a random property from an object
|
||||
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
|
||||
returns random property (the value)
|
||||
*/
|
||||
|
||||
randomVal: function(obj, options) {
|
||||
var count, key, result, val;
|
||||
result = void 0;
|
||||
count = 0;
|
||||
for (key in obj) {
|
||||
val = obj[key];
|
||||
if (user.fns.predictableRandom(options != null ? options.seed : void 0) < (1 / ++count)) {
|
||||
result = ((options != null ? options.key : void 0) ? key : val);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/*
|
||||
This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
|
||||
user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
|
||||
so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where
|
||||
|
|
@ -11477,8 +11485,9 @@ var process=require("__browserify_process");(function() {
|
|||
}), 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;
|
||||
var a, acceptableDrops, alpha, chanceMultiplier, delta, drop, max, priority, rarity, reachedDropLimit, streak, _base, _base1, _base2, _base3, _name, _name1, _name2, _ref, _ref1;
|
||||
delta = modifiers.delta;
|
||||
_ref = modifiers.task, priority = _ref.priority, streak = _ref.streak;
|
||||
if (streak == null) {
|
||||
streak = 0;
|
||||
}
|
||||
|
|
@ -11498,10 +11507,10 @@ var process=require("__browserify_process");(function() {
|
|||
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 (((_ref1 = user.flags) != null ? _ref1.dropsEnabled : void 0) && user.fns.predictableRandom() < alpha) {
|
||||
rarity = user.fns.predictableRandom();
|
||||
if (rarity > .6) {
|
||||
drop = randomVal(_.omit(content.food, 'Saddle'));
|
||||
drop = user.fns.randomVal(_.omit(content.food, 'Saddle'));
|
||||
if ((_base1 = user.items.food)[_name = drop.name] == null) {
|
||||
_base1[_name] = 0;
|
||||
}
|
||||
|
|
@ -11509,7 +11518,7 @@ var process=require("__browserify_process");(function() {
|
|||
drop.type = 'Food';
|
||||
drop.dialog = "You've found a " + drop.text + " Food! " + drop.notes;
|
||||
} else if (rarity > .3) {
|
||||
drop = randomVal(content.eggs);
|
||||
drop = user.fns.randomVal(content.eggs);
|
||||
if ((_base2 = user.items.eggs)[_name1 = drop.name] == null) {
|
||||
_base2[_name1] = 0;
|
||||
}
|
||||
|
|
@ -11518,7 +11527,7 @@ var process=require("__browserify_process");(function() {
|
|||
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) {
|
||||
drop = user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) {
|
||||
return __indexOf.call(acceptableDrops, k) >= 0;
|
||||
})));
|
||||
if ((_base3 = user.items.hatchingPotions)[_name2 = drop.name] == null) {
|
||||
|
|
|
|||
|
|
@ -57,24 +57,6 @@ api.shouldDo = (day, repeat, options={}) ->
|
|||
yesterday = moment(o.now).subtract(1,'d').day() # have to wrap o.now so as not to modify original
|
||||
return repeat[dayMapping[yesterday]] # FIXME is this correct?? Do I need to do any timezone calcaulation here?
|
||||
|
||||
###
|
||||
------------------------------------------------------
|
||||
Drop System
|
||||
------------------------------------------------------
|
||||
###
|
||||
|
||||
###
|
||||
Get a random property from an object
|
||||
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
|
||||
returns random property (the value)
|
||||
###
|
||||
randomVal = (obj) ->
|
||||
result = undefined
|
||||
count = 0
|
||||
for key, val of obj
|
||||
result = val if Math.random() < (1 / ++count)
|
||||
result
|
||||
|
||||
|
||||
###
|
||||
------------------------------------------------------
|
||||
|
|
@ -87,8 +69,6 @@ api.tnl = (lvl) ->
|
|||
else Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 10
|
||||
# round to nearest 10?
|
||||
|
||||
|
||||
|
||||
###
|
||||
Preen history for users with > 7 history entries
|
||||
This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
|
||||
|
|
@ -445,12 +425,13 @@ api.wrap = (user) ->
|
|||
user.stats.lvl-- if user.stats.lvl > 1
|
||||
|
||||
# Lose a stat point
|
||||
lostStat = randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {})
|
||||
lostStat = user.fns.randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {})
|
||||
user.stats[lostStat]-- if lostStat
|
||||
|
||||
# Lose a gear piece
|
||||
# Note, they can actually lose item weapon_*_0 - it's 0 to buy back, no big deal
|
||||
lostItem = randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[k]=k if v;m), {})
|
||||
# Note the `""+` string-casting. Without this, when run on the server Mongoose returns funny objects
|
||||
lostItem = user.fns.randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[""+k]=""+k if v;m), {})
|
||||
if item = content.gear.flat[lostItem]
|
||||
user.items.gear.owned[lostItem] = false
|
||||
user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem
|
||||
|
|
@ -644,7 +625,7 @@ api.wrap = (user) ->
|
|||
|
||||
# Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results)
|
||||
if typeof window is 'undefined'
|
||||
user.fns.randomDrop({task, delta, priority:task.priority, streak:task.streak}) if direction is 'up'
|
||||
user.fns.randomDrop({task, delta}) if direction is 'up'
|
||||
|
||||
cb? null, req
|
||||
return delta
|
||||
|
|
@ -681,6 +662,28 @@ api.wrap = (user) ->
|
|||
when 'potion' then 5
|
||||
else 6
|
||||
|
||||
###
|
||||
Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
|
||||
we need things to be "random", but technically predictable so that they don't go out-of-sync
|
||||
###
|
||||
predictableRandom: (seed) ->
|
||||
# Default seed is all user stats combined. Fairly safe, meh - pass in a good seed for situations where that doesn't work
|
||||
seed = _.reduce(user.stats, ((m,v)->if _.isNumber(v) then m+v else m), 0) if !seed or seed is Math.PI
|
||||
x = Math.sin(seed++) * 10000
|
||||
x - Math.floor(x)
|
||||
|
||||
###
|
||||
Get a random property from an object
|
||||
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
|
||||
returns random property (the value)
|
||||
###
|
||||
randomVal: (obj, options) ->
|
||||
result = undefined
|
||||
count = 0
|
||||
for key, val of obj
|
||||
result = (if options?.key then key else val) if user.fns.predictableRandom(options?.seed) < (1 / ++count)
|
||||
result
|
||||
|
||||
###
|
||||
This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
|
||||
user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
|
||||
|
|
@ -705,7 +708,8 @@ api.wrap = (user) ->
|
|||
# ----------------------------------------------------------------------
|
||||
|
||||
randomDrop: (modifiers) ->
|
||||
{delta, priority, streak} = modifiers
|
||||
{delta} = modifiers
|
||||
{priority, streak} = modifiers.task
|
||||
streak ?= 0
|
||||
# limit drops to 2 / day
|
||||
user.items.lastDrop ?=
|
||||
|
|
@ -728,14 +732,14 @@ api.wrap = (user) ->
|
|||
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
|
||||
if user.flags?.dropsEnabled and user.fns.predictableRandom() < 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()
|
||||
rarity = user.fns.predictableRandom()
|
||||
|
||||
# Food: 40% chance
|
||||
if rarity > .6
|
||||
drop = randomVal _.omit(content.food, 'Saddle')
|
||||
drop = user.fns.randomVal _.omit(content.food, 'Saddle')
|
||||
user.items.food[drop.name] ?= 0
|
||||
user.items.food[drop.name]+= 1
|
||||
drop.type = 'Food'
|
||||
|
|
@ -743,7 +747,7 @@ api.wrap = (user) ->
|
|||
|
||||
# Eggs: 30% chance
|
||||
else if rarity > .3
|
||||
drop = randomVal content.eggs
|
||||
drop = user.fns.randomVal content.eggs
|
||||
user.items.eggs[drop.name] ?= 0
|
||||
user.items.eggs[drop.name]++
|
||||
drop.type = 'Egg'
|
||||
|
|
@ -763,7 +767,7 @@ api.wrap = (user) ->
|
|||
|
||||
# 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))
|
||||
drop = user.fns.randomVal _.pick(content.hatchingPotions, ((v,k) -> k in acceptableDrops))
|
||||
|
||||
user.items.hatchingPotions[drop.name] ?= 0
|
||||
user.items.hatchingPotions[drop.name]++
|
||||
|
|
|
|||
Loading…
Reference in a new issue