wip(shared): port buy ops and linked fns

This commit is contained in:
Matteo Pagliazzi
2016-03-16 16:25:46 +01:00
parent e68ebee980
commit ff72706cae
11 changed files with 227 additions and 215 deletions

View File

@@ -77,6 +77,7 @@
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
"questNotFound": "Quest \"<%= key %>\" not found.",
"questNotOwned": "You don't own that quest scroll.",
"questNotGoldPurchasable": "Quest \"<%= key %>\" is not a Gold-purchasable quest.",
"questLevelTooHigh": "You must be level <%= level %> to begin this quest.",
"questAlreadyUnderway": "Your party is already on a quest. Try again when the current quest has ended.",
"questAlreadyAccepted": "You already accepted the quest invitation.",
@@ -102,5 +103,9 @@
"spellNotOwned": "You don't own this spell.",
"spellLevelTooHigh": "You must be level <%= level %> to use this spell.",
"invalidAttribute": "\"<%= attr %>\" is not a valid attribute.",
"notEnoughAttrPoints": "You don't have enough attribute points."
"notEnoughAttrPoints": "You don't have enough attribute points.",
"missingKeyParam": "\"req.params.key\" is required.",
"mysterySetNotFound": "Mystery set not found, or set already owned",
"itemNotFound": "Item \"<%= key %>\" not found.",
"cannoyBuyItem": "You can't buy this item"
}

View File

@@ -1,24 +1,23 @@
import content from '../content/index';
import i18n from '../i18n';
module.exports = function(user, item, type, req) {
var message, currentWeapon, currentShield;
if (type == null) {
type = 'equipped';
}
currentShield = content.gear.flat[user.items.gear[type].shield];
currentWeapon = content.gear.flat[user.items.gear[type].weapon];
module.exports = function handleTwoHanded (user, item, type = 'equipped', req) {
let currentShield = content.gear.flat[user.items.gear[type].shield];
let currentWeapon = content.gear.flat[user.items.gear[type].weapon];
if (item.type === "shield" && (currentWeapon ? currentWeapon.twoHanded : false)) {
let message;
if (item.type === 'shield' && (currentWeapon ? currentWeapon.twoHanded : false)) {
user.items.gear[type].weapon = 'weapon_base_0';
message = i18n.t('messageTwoHandedUnequip', {
twoHandedText: currentWeapon.text(req.language), offHandedText: item.text(req.language),
}, req.language);
} else if (item.twoHanded && (currentShield && user.items.gear[type].shield != "shield_base_0")) {
user.items.gear[type].shield = "shield_base_0";
} else if (item.twoHanded && (currentShield && user.items.gear[type].shield !== 'shield_base_0')) {
user.items.gear[type].shield = 'shield_base_0';
message = i18n.t('messageTwoHandedEquip', {
twoHandedText: item.text(req.language), offHandedText: currentShield.text(req.language),
}, req.language);
}
return message;
};

View File

@@ -1,20 +1,19 @@
import _ from 'lodash';
/*
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
*/
module.exports = function(user, seed) {
var x;
// 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
module.exports = function predictableRandom (user, seed) {
if (!seed || seed === Math.PI) {
seed = _.reduce(user.stats, (function(m, v) {
if (_.isNumber(v)) {
return m + v;
seed = _.reduce(user.stats, (accumulator, val) => {
if (_.isNumber(val)) {
return accumulator + val;
} else {
return m;
return accumulator;
}
}), 0);
}, 0);
}
x = Math.sin(seed++) * 10000;
let x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
};

View File

@@ -1,14 +1,12 @@
import _ from 'lodash';
import predictableRandom from './predictableRandom';
/*
Get a random property from an object
returns random property (the value)
*/
// Get a random property from an object
// returns random property (the value)
module.exports = function(user, obj, options) {
var array, rand;
array = (options != null ? options.key : void 0) ? _.keys(obj) : _.values(obj);
rand = user.fns.predictableRandom(options != null ? options.seed : void 0);
module.exports = function randomVal (user, obj, options = {}) {
let array = options.key ? _.keys(obj) : _.values(obj);
let rand = predictableRandom(user, options.seed);
array.sort();
return array[Math.floor(rand * array.length)];
};

View File

@@ -1,33 +1,35 @@
import content from '../content/index';
import _ from 'lodash';
module.exports = function(user) {
var base, owned;
owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject();
if ((base = user.achievements).ultimateGearSets == null) {
base.ultimateGearSets = {
module.exports = function ultimateGear (user) {
let owned = window ? user.items.gear.owned : user.items.gear.owned.toObject();
if (!user.achievements.ultimateGearSets) {
user.achievements.ultimateGearSets = {
healer: false,
wizard: false,
rogue: false,
warrior: false
warrior: false,
};
}
content.classes.forEach(function(klass) {
content.classes.forEach((klass) => {
if (user.achievements.ultimateGearSets[klass] !== true) {
return user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], function(soFarGood, type) {
var found;
found = _.find(content.gear.tree[type][klass], {
last: true
user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], (soFarGood, type) => {
let found = _.find(content.gear.tree[type][klass], {
last: true,
});
return soFarGood && (!found || owned[found.key] === true);
}, true);
}
});
if (typeof user.markModified === "function") {
user.markModified('achievements.ultimateGearSets');
}
// TODO
if (user.markModified) user.markModified('achievements.ultimateGearSets');
if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) {
user.flags.armoireEnabled = true;
return typeof user.markModified === "function" ? user.markModified('flags') : void 0;
}
return;
};

View File

@@ -3,117 +3,133 @@ import i18n from '../i18n';
import _ from 'lodash';
import count from '../count';
import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
import predictableRandom from '../fns/predictableRandom';
import randomVal from '../fns/randomVal';
import handleTwoHanded from '../fns/handleTwoHanded';
import ultimateGear from '../fns/ultimateGear';
module.exports = function(user, req, cb, analytics) {
var analyticsData, armoireExp, armoireResp, armoireResult, base, buyResp, drop, eligibleEquipment, item, key, message, name;
key = req.params.key;
item = key === 'potion' ? content.potion : key === 'armoire' ? content.armoire : content.gear.flat[key];
if (!item) {
return typeof cb === "function" ? cb({
code: 404,
message: "Item '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
}) : void 0;
module.exports = function buy (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item;
if (key === 'potion') {
item = content.potion;
} else if (key === 'armoire') {
item = content.armoire;
} else {
item = content.gear.flat[key];
}
if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
if (user.stats.gp < item.value) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
if ((item.canOwn != null) && !item.canOwn(user)) {
return typeof cb === "function" ? cb({
code: 401,
message: "You can't buy this item"
}) : void 0;
if (item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(i18n.t('cannoyBuyItem', req.language));
}
armoireResp = void 0;
let armoireResp;
let armoireResult;
let eligibleEquipment;
let drop;
let message;
if (item.key === 'potion') {
user.stats.hp += 15;
if (user.stats.hp > 50) {
user.stats.hp = 50;
}
} else if (item.key === 'armoire') {
armoireResult = user.fns.predictableRandom(user.stats.gp);
eligibleEquipment = _.filter(content.gear.flat, (function(i) {
return i.klass === 'armoire' && !user.items.gear.owned[i.key];
}));
if (!_.isEmpty(eligibleEquipment) && (armoireResult < .6 || !user.flags.armoireOpened)) {
armoireResult = predictableRandom(user, user.stats.gp);
eligibleEquipment = _.filter(content.gear.flat, (eligible) => {
return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key];
});
if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) {
eligibleEquipment.sort();
drop = user.fns.randomVal(eligibleEquipment);
drop = randomVal(user, eligibleEquipment);
user.items.gear.owned[drop.key] = true;
user.flags.armoireOpened = true;
message = i18n.t('armoireEquipment', {
image: '<span class="shop_' + drop.key + ' pull-left"></span>',
dropText: drop.text(req.language)
image: `<span class="shop_${drop.key} pull-left"></span>`,
dropText: drop.text(req.language),
}, req.language);
if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
user.flags.armoireEmpty = true;
}
armoireResp = {
type: "gear",
type: 'gear',
dropKey: drop.key,
dropText: drop.text(req.language)
dropText: drop.text(req.language),
};
} else if ((!_.isEmpty(eligibleEquipment) && armoireResult < .8) || armoireResult < .5) {
drop = user.fns.randomVal(_.where(content.food, {
canDrop: true
} else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
drop = randomVal(_.where(content.food, {
canDrop: true,
}));
if ((base = user.items.food)[name = drop.key] == null) {
base[name] = 0;
}
user.items.food[drop.key] = user.items.food[drop.key] || 0;
user.items.food[drop.key] += 1;
message = i18n.t('armoireFood', {
image: '<span class="Pet_Food_' + drop.key + ' pull-left"></span>',
image: `<span class="Pet_Food_${drop.key} pull-left"></span>`,
dropArticle: drop.article,
dropText: drop.text(req.language)
dropText: drop.text(req.language),
}, req.language);
armoireResp = {
type: "food",
type: 'food',
dropKey: drop.key,
dropArticle: drop.article,
dropText: drop.text(req.language)
dropText: drop.text(req.language),
};
} else {
armoireExp = Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10);
let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10);
user.stats.exp += armoireExp;
message = i18n.t('armoireExp', req.language);
armoireResp = {
"type": "experience",
"value": armoireExp
type: 'experience',
value: armoireExp,
};
}
} else {
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
message = user.fns.handleTwoHanded(item, null, req);
message = handleTwoHanded(user, item, null, req);
}
user.items.gear.owned[item.key] = true;
if (message == null) {
if (!message) {
message = i18n.t('messageBought', {
itemText: item.text(req.language)
itemText: item.text(req.language),
}, req.language);
}
if (item.last) {
user.fns.ultimateGear();
}
if (item.last) ultimateGear(user);
}
user.stats.gp -= item.value;
analyticsData = {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior'
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
});
}
let buyResp = _.pick(user, splitWhitespace('items achievements stats flags'));
if (armoireResp) buyResp.armoire = armoireResp;
return {
data: buyResp,
message,
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
buyResp = _.pick(user, splitWhitespace('items achievements stats flags'));
if (armoireResp) {
buyResp["armoire"] = armoireResp;
}
return typeof cb === "function" ? cb({
code: 200,
message: message
}, buyResp) : void 0;
};

View File

@@ -2,42 +2,48 @@ import i18n from '../i18n';
import content from '../content/index';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
module.exports = function buyMysterySet (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
module.exports = function(user, req, cb, analytics) {
var mysterySet, ref;
if (!(user.purchased.plan.consecutive.trinkets > 0)) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughHourglasses', req.language)
}) : void 0;
}
mysterySet = (ref = content.timeTravelerStore(user.items.gear.owned)) != null ? ref[req.params.key] : void 0;
if ((typeof window !== "undefined" && window !== null ? window.confirm : void 0) != null) {
if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) {
return;
}
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
}
let ref = content.timeTravelerStore(user.items.gear.owned);
let mysterySet = ref ? ref[key] : undefined;
if (!mysterySet) {
return typeof cb === "function" ? cb({
code: 404,
message: "Mystery set not found, or set already owned"
}) : void 0;
throw new NotFound(i18n.t('mysterySetNotFound', req.language));
}
_.each(mysterySet.items, function(i) {
var analyticsData;
user.items.gear.owned[i.key] = true;
analyticsData = {
uuid: user._id,
itemKey: i.key,
itemType: 'Subscriber Gear',
acquireMethod: 'Hourglass',
category: 'behavior'
};
return analytics != null ? analytics.track('acquire item', analyticsData) : void 0;
if (window && window.confirm) { // TODO move to client
if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) return;
}
_.each(mysterySet.items, item => {
user.items.gear.owned[item.key] = true;
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: item.key,
itemType: 'Subscriber Gear',
acquireMethod: 'Hourglass',
category: 'behavior',
});
}
});
user.purchased.plan.consecutive.trinkets--;
return typeof cb === "function" ? cb({
code: 200,
message: i18n.t('hourglassPurchaseSet', req.language)
}, _.pick(user, splitWhitespace('items purchased.plan.consecutive'))) : void 0;
return {
data: _.pick(user, splitWhitespace('items purchased.plan.consecutive')),
message: i18n.t('hourglassPurchaseSet', req.language),
};
};

View File

@@ -1,49 +1,44 @@
import i18n from '../i18n';
import content from '../content/index';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
import _ from 'lodash';
module.exports = function buyQuest (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.quests[key];
if (!item) throw new NotFound(i18n.t('questNotFound', req.language));
module.exports = function(user, req, cb, analytics) {
var analyticsData, base, item, key, message, name;
key = req.params.key;
item = content.quests[key];
if (!item) {
return typeof cb === "function" ? cb({
code: 404,
message: "Quest '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
}) : void 0;
}
if (!(item.category === 'gold' && item.goldValue)) {
return typeof cb === "function" ? cb({
code: 404,
message: "Quest '" + key + " is not a Gold-purchasable quest (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
}) : void 0;
throw new NotAuthorized(i18n.t('questNotGoldPurchasable', {key}, req.language));
}
if (user.stats.gp < item.goldValue) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
message = i18n.t('messageBought', {
itemText: item.text(req.language)
}, req.language);
if ((base = user.items.quests)[name = item.key] == null) {
base[name] = 0;
}
user.items.quests[item.key] += 1;
user.items.quests[item.key] = user.items.quests[item.key] || 0;
user.items.quests[item.key]++;
user.stats.gp -= item.goldValue;
analyticsData = {
uuid: user._id,
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
acquireMethod: 'Gold',
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
acquireMethod: 'Gold',
category: 'behavior',
});
}
return typeof cb === "function" ? cb({
code: 200,
message: message
}, user.items.quests) : void 0;
return {
data: user.items.quests,
message: i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
};
};

View File

@@ -2,30 +2,30 @@ import i18n from '../i18n';
import content from '../content/index';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
module.exports = function buySpecialSpell (user, req = {}) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.special[key];
if (!item) throw new NotFound(i18n.t('spellNotFound', {spellId: key}, req.language));
module.exports = function(user, req, cb) {
var base, item, key, message;
key = req.params.key;
item = content.special[key];
if (user.stats.gp < item.value) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
user.stats.gp -= item.value;
if ((base = user.items.special)[key] == null) {
base[key] = 0;
}
user.items.special[key]++;
if (typeof user.markModified === "function") {
user.markModified('items.special');
}
message = i18n.t('messageBought', {
itemText: item.text(req.language)
}, req.language);
return typeof cb === "function" ? cb({
code: 200,
message: message
}, _.pick(user, splitWhitespace('items stats'))) : void 0;
return {
data: _.pick(user, splitWhitespace('items stats')),
message: i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
};
};

View File

@@ -21,10 +21,6 @@ const COMMON_FILES = [
'!./common/script/ops/addWebhook.js',
'!./common/script/ops/allocateNow.js',
'!./common/script/ops/blockUser.js',
'!./common/script/ops/buy.js',
'!./common/script/ops/buyMysterySet.js',
'!./common/script/ops/buyQuest.js',
'!./common/script/ops/buySpecialSpell.js',
'!./common/script/ops/changeClass.js',
'!./common/script/ops/clearCompleted.js',
'!./common/script/ops/clearPMs.js',
@@ -63,13 +59,9 @@ const COMMON_FILES = [
'!./common/script/fns/dotGet.js',
'!./common/script/fns/dotSet.js',
'!./common/script/fns/getItem.js',
'!./common/script/fns/handleTwoHanded.js',
'!./common/script/fns/nullify.js',
'!./common/script/fns/predictableRandom.js',
'!./common/script/fns/preenUserHistory.js',
'!./common/script/fns/randomDrop.js',
'!./common/script/fns/randomVal.js',
'!./common/script/fns/ultimateGear.js',
'!./common/script/fns/updateStats.js',
'!./common/script/libs/appliedTags.js',
'!./common/script/libs/countExists.js',

View File

@@ -263,15 +263,15 @@ export let schema = new Schema({
spookDust: {type: Number, default: 0},
shinySeed: {type: Number, default: 0},
seafoam: {type: Number, default: 0},
valentine: Number,
valentine: {type: Number, default: 0},
valentineReceived: Array, // array of strings, by sender name
nye: Number,
nye: {type: Number, default: 0},
nyeReceived: Array,
greeting: Number,
greeting: {type: Number, default: 0},
greetingReceived: Array,
thankyou: Number,
thankyou: {type: Number, default: 0},
thankyouReceived: Array,
birthday: Number,
birthday: {type: Number, default: 0},
birthdayReceived: Array,
},