Files
habitica/website/common/script/content/spells.js
Alexandrea Beh da60f44356 Issue 12220 stealth tooltip show protected dailies (#12255)
* Issue 12220 - Show dailies protected by Stealth

A feature on the old site showed the number of
dailies protected by casting Stealth. This is
now showing again in the Stealth tooltip. The
skillNotes section was not being called. It
adds additional skill-specific info: the dailies
avoided by stealth, whether stealth no longer
needs to be cast (dailies already avoided) and
whether frost is no longer useful to cast.

Speculation: the spellDisabled method had some
commented out code regarding Stealth, which
may have broken due to a change in how dailies
are referenced. I have fixed this line, so it
seemed alright to keep the entirety of the
skillNotes function as it had been. However,
this includes more than just showing dailies
in the tooltip.

Behavior changes:
- tooltip shows dailies avoided for Stealth
- tooltip shows maxed out message for
Stealth when all dailies are covered
- tooltip shows frost already cast message
when frost has already been cast

* clean up conditions in skillNotes

* use future tense for rogue stealth dailies tooltip

* use getter for accurate task length

* consider stealth disabled based on incomplete dailies due, not all dailies

* Issue 12220 - Show dailies protected by Stealth

A feature on the old site showed the number of
dailies protected by casting Stealth. This is
now showing again in the Stealth tooltip. The
skillNotes section was not being called. It
adds additional skill-specific info: the dailies
avoided by stealth, whether stealth no longer
needs to be cast (dailies already avoided) and
whether frost is no longer useful to cast.

Speculation: the spellDisabled method had some
commented out code regarding Stealth, which
may have broken due to a change in how dailies
are referenced. I have fixed this line, so it
seemed alright to keep the entirety of the
skillNotes function as it had been. However,
this includes more than just showing dailies
in the tooltip.

Behavior changes:
- tooltip shows dailies avoided for Stealth
- tooltip shows maxed out message for
Stealth when all dailies are covered
- tooltip shows frost already cast message
when frost has already been cast

* factor out stealthBuffsToAdd for casting stealth + tooltip previewing dailies avoided

* fix merge conflict
2020-06-29 11:12:19 +02:00

721 lines
21 KiB
JavaScript

import each from 'lodash/each';
import t from './translation';
import { NotAuthorized } from '../libs/errors';
import statsComputed from '../libs/statsComputed'; // eslint-disable-line import/no-cycle
import setDebuffPotionItems from '../libs/setDebuffPotionItems'; // eslint-disable-line import/no-cycle
import crit from '../fns/crit'; // eslint-disable-line import/no-cycle
import updateStats from '../fns/updateStats';
/*
---------------------------------------------------------------
Spells
---------------------------------------------------------------
Text, notes, and mana are obvious. The rest:
* {target}: one of [task, self, party, user].
* This is very important, because if the cast() function is expecting one
thing and receives another, it will cause errors.
`self` is used for self buffs, multi-task debuffs, AOEs (eg, meteor-shower),
etc. Basically, use self for anything that's not [task, party, user] and is an instant-cast
* {cast}: the function that's run to perform the ability's action.
This is pretty slick - because this is exported to the
web, this function can be performed on the client and on the server.
`user` param is self (needed for determining your
own stats for effectiveness of cast), and `target` param is one of [task, party, user].
In the case of `self` skills,
you act on `user` instead of `target`. You can trust these are the correct objects,
as long as the `target` attr of the
spell is correct. Take a look at habitrpg/website/server/models/user.js and
habitrpg/website/server/models/task.js for what attributes are
available on each model. Note `task.value` is its "redness".
If party is passed in, it's an array of users,
so you'll want to iterate over them like: `_.each(target,function(member){...})`
Note, user.stats.mp is docked after automatically
(it's appended to functions automatically down below in an _.each)
*/
function diminishingReturns (bonus, max, halfway) {
if (!halfway) halfway = max / 2; // eslint-disable-line no-param-reassign
return max * (bonus / (bonus + halfway));
}
function calculateBonus (value, stat, critVal = 1, statScale = 0.5) {
return (value < 0 ? 1 : value + 1) + stat * statScale * critVal;
}
export function stealthBuffsToAdd (user) {
return Math.ceil(diminishingReturns(
statsComputed(user).per, user.tasksOrder.dailys.length * 0.64, 55,
));
}
const spells = {};
spells.wizard = {
fireball: { // Burst of Flames
text: t('spellWizardFireballText'),
mana: 10,
lvl: 11,
target: 'task',
notes: t('spellWizardFireballNotes'),
cast (user, target, req) {
let bonus = statsComputed(user).int * crit.crit(user, 'per');
bonus *= Math.ceil((target.value < 0 ? 1 : target.value + 1) * 0.075);
user.stats.exp += diminishingReturns(bonus, 75);
if (!user.party.quest.progress.up) user.party.quest.progress.up = 0;
user.party.quest.progress.up += Math.ceil(statsComputed(user).int * 0.1);
updateStats(user, user.stats, req);
},
},
mpheal: { // Ethereal Surge
text: t('spellWizardMPHealText'),
mana: 30,
lvl: 12,
target: 'party',
notes: t('spellWizardMPHealNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).int;
if (user._id !== member._id && member.stats.class !== 'wizard') {
member.stats.mp += Math.ceil(diminishingReturns(bonus, 25, 125));
}
});
},
},
earth: { // Earthquake
text: t('spellWizardEarthText'),
mana: 35,
lvl: 13,
target: 'party',
notes: t('spellWizardEarthNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).int - user.stats.buffs.int;
if (!member.stats.buffs.int) member.stats.buffs.int = 0;
member.stats.buffs.int += Math.ceil(diminishingReturns(bonus, 30, 200));
});
},
},
frost: { // Chilling Frost
text: t('spellWizardFrostText'),
mana: 40,
lvl: 14,
target: 'self',
notes: t('spellWizardFrostNotes'),
cast (user) {
user.stats.buffs.streaks = true;
},
},
};
spells.warrior = {
smash: { // Brutal Smash
text: t('spellWarriorSmashText'),
mana: 10,
lvl: 11,
target: 'task',
notes: t('spellWarriorSmashNotes'),
cast (user, target) {
const bonus = statsComputed(user).str * crit.crit(user, 'con');
target.value += diminishingReturns(bonus, 2.5, 35);
if (!user.party.quest.progress.up) user.party.quest.progress.up = 0;
user.party.quest.progress.up += diminishingReturns(bonus, 55, 70);
},
},
defensiveStance: { // Defensive Stance
text: t('spellWarriorDefensiveStanceText'),
mana: 25,
lvl: 12,
target: 'self',
notes: t('spellWarriorDefensiveStanceNotes'),
cast (user) {
const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!user.stats.buffs.con) user.stats.buffs.con = 0;
user.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 40, 200));
},
},
valorousPresence: { // Valorous Presence
text: t('spellWarriorValorousPresenceText'),
mana: 20,
lvl: 13,
target: 'party',
notes: t('spellWarriorValorousPresenceNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).str - user.stats.buffs.str;
if (!member.stats.buffs.str) member.stats.buffs.str = 0;
member.stats.buffs.str += Math.ceil(diminishingReturns(bonus, 20, 200));
});
},
},
intimidate: { // Intimidating Gaze
text: t('spellWarriorIntimidateText'),
mana: 15,
lvl: 14,
target: 'party',
notes: t('spellWarriorIntimidateNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!member.stats.buffs.con) member.stats.buffs.con = 0;
member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 24, 200));
});
},
},
};
spells.rogue = {
pickPocket: { // Pickpocket
text: t('spellRoguePickPocketText'),
mana: 10,
lvl: 11,
target: 'task',
notes: t('spellRoguePickPocketNotes'),
cast (user, target) {
const bonus = calculateBonus(target.value, statsComputed(user).per);
user.stats.gp += diminishingReturns(bonus, 25, 75);
},
},
backStab: { // Backstab
text: t('spellRogueBackStabText'),
mana: 15,
lvl: 12,
target: 'task',
notes: t('spellRogueBackStabNotes'),
cast (user, target, req) {
const _crit = crit.crit(user, 'str', 0.3);
const bonus = calculateBonus(target.value, statsComputed(user).str, _crit);
user.stats.exp += diminishingReturns(bonus, 75, 50);
user.stats.gp += diminishingReturns(bonus, 18, 75);
updateStats(user, user.stats, req);
},
},
toolsOfTrade: { // Tools of the Trade
text: t('spellRogueToolsOfTradeText'),
mana: 25,
lvl: 13,
target: 'party',
notes: t('spellRogueToolsOfTradeNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).per - user.stats.buffs.per;
if (!member.stats.buffs.per) member.stats.buffs.per = 0;
member.stats.buffs.per += Math.ceil(diminishingReturns(bonus, 100, 50));
});
},
},
stealth: { // Stealth
text: t('spellRogueStealthText'),
mana: 45,
lvl: 14,
target: 'self',
notes: t('spellRogueStealthNotes'),
cast (user) {
if (!user.stats.buffs.stealth) user.stats.buffs.stealth = 0;
user.stats.buffs.stealth += stealthBuffsToAdd(user);
},
},
};
spells.healer = {
heal: { // Healing Light
text: t('spellHealerHealText'),
mana: 15,
lvl: 11,
target: 'self',
notes: t('spellHealerHealNotes'),
cast (user) {
if (user.stats.hp >= 50) throw new NotAuthorized(t('messageHealthAlreadyMax')(user.language));
user.stats.hp += (statsComputed(user).con + statsComputed(user).int + 5) * 0.075;
if (user.stats.hp > 50) user.stats.hp = 50;
},
},
brightness: { // Searing Brightness
text: t('spellHealerBrightnessText'),
mana: 15,
lvl: 12,
target: 'tasks',
notes: t('spellHealerBrightnessNotes'),
cast (user, tasks) {
each(tasks, task => {
if (task.type !== 'reward') {
task.value += 4 * (statsComputed(user).int / (statsComputed(user).int + 40));
}
});
},
},
protectAura: { // Protective Aura
text: t('spellHealerProtectAuraText'),
mana: 30,
lvl: 13,
target: 'party',
notes: t('spellHealerProtectAuraNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!member.stats.buffs.con) member.stats.buffs.con = 0;
member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 200, 200));
});
},
},
healAll: { // Blessing
text: t('spellHealerHealAllText'),
mana: 25,
lvl: 14,
target: 'party',
notes: t('spellHealerHealAllNotes'),
cast (user, target) {
each(target, member => {
member.stats.hp += (statsComputed(user).con + statsComputed(user).int + 5) * 0.04;
if (member.stats.hp > 50) member.stats.hp = 50;
});
},
},
};
spells.special = {
snowball: {
text: t('spellSpecialSnowballAuraText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialSnowballAuraNotes'),
cast (user, target, req) {
if (!user.items.special.snowball) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = true;
target.stats.buffs.spookySparkles = false;
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = false;
if (!target.achievements.snowball) target.achievements.snowball = 0;
target.achievements.snowball += 1;
user.items.special.snowball -= 1;
setDebuffPotionItems(user);
},
},
salt: {
text: t('spellSpecialSaltText'),
mana: 0,
value: 5,
immediateUse: true,
purchaseType: 'debuffPotion',
target: 'self',
notes: t('spellSpecialSaltNotes'),
cast (user) {
user.stats.buffs.snowball = false;
user.stats.gp -= 5;
setDebuffPotionItems(user);
},
},
spookySparkles: {
text: t('spellSpecialSpookySparklesText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialSpookySparklesNotes'),
cast (user, target, req) {
if (!user.items.special.spookySparkles) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = false;
target.stats.buffs.spookySparkles = true;
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = false;
if (!target.achievements.spookySparkles) target.achievements.spookySparkles = 0;
target.achievements.spookySparkles += 1;
user.items.special.spookySparkles -= 1;
setDebuffPotionItems(user);
},
},
opaquePotion: {
text: t('spellSpecialOpaquePotionText'),
mana: 0,
value: 5,
immediateUse: true,
purchaseType: 'debuffPotion',
target: 'self',
notes: t('spellSpecialOpaquePotionNotes'),
cast (user) {
user.stats.buffs.spookySparkles = false;
user.stats.gp -= 5;
setDebuffPotionItems(user);
},
},
shinySeed: {
text: t('spellSpecialShinySeedText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialShinySeedNotes'),
cast (user, target, req) {
if (!user.items.special.shinySeed) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = false;
target.stats.buffs.spookySparkles = false;
target.stats.buffs.shinySeed = true;
target.stats.buffs.seafoam = false;
if (!target.achievements.shinySeed) target.achievements.shinySeed = 0;
target.achievements.shinySeed += 1;
user.items.special.shinySeed -= 1;
setDebuffPotionItems(user);
},
},
petalFreePotion: {
text: t('spellSpecialPetalFreePotionText'),
mana: 0,
value: 5,
immediateUse: true,
purchaseType: 'debuffPotion',
target: 'self',
notes: t('spellSpecialPetalFreePotionNotes'),
cast (user) {
user.stats.buffs.shinySeed = false;
user.stats.gp -= 5;
setDebuffPotionItems(user);
},
},
seafoam: {
text: t('spellSpecialSeafoamText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialSeafoamNotes'),
cast (user, target, req) {
if (!user.items.special.seafoam) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = false;
target.stats.buffs.spookySparkles = false;
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = true;
if (!target.achievements.seafoam) target.achievements.seafoam = 0;
target.achievements.seafoam += 1;
user.items.special.seafoam -= 1;
setDebuffPotionItems(user);
},
},
sand: {
text: t('spellSpecialSandText'),
mana: 0,
value: 5,
immediateUse: true,
purchaseType: 'debuffPotion',
target: 'self',
notes: t('spellSpecialSandNotes'),
cast (user) {
user.stats.buffs.seafoam = false;
user.stats.gp -= 5;
setDebuffPotionItems(user);
},
},
nye: {
text: t('nyeCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('nyeCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.nye) user.achievements.nye = 0;
user.achievements.nye += 1;
} else {
each([user, target], u => {
if (!u.achievements.nye) u.achievements.nye = 0;
u.achievements.nye += 1;
});
}
if (!target.items.special.nyeReceived) target.items.special.nyeReceived = [];
const senderName = user.profile.name;
target.items.special.nyeReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'nye',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
valentine: {
text: t('valentineCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('valentineCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.valentine) user.achievements.valentine = 0;
user.achievements.valentine += 1;
} else {
each([user, target], u => {
if (!u.achievements.valentine) u.achievements.valentine = 0;
u.achievements.valentine += 1;
});
}
if (!target.items.special.valentineReceived) target.items.special.valentineReceived = [];
const senderName = user.profile.name;
target.items.special.valentineReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'valentine',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
greeting: {
text: t('greetingCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('greetingCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.greeting) user.achievements.greeting = 0;
user.achievements.greeting += 1;
} else {
each([user, target], u => {
if (!u.achievements.greeting) u.achievements.greeting = 0;
u.achievements.greeting += 1;
});
}
if (!target.items.special.greetingReceived) target.items.special.greetingReceived = [];
const senderName = user.profile.name;
target.items.special.greetingReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'greeting',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
thankyou: {
text: t('thankyouCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('thankyouCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.thankyou) user.achievements.thankyou = 0;
user.achievements.thankyou += 1;
} else {
each([user, target], u => {
if (!u.achievements.thankyou) u.achievements.thankyou = 0;
u.achievements.thankyou += 1;
});
}
if (!target.items.special.thankyouReceived) target.items.special.thankyouReceived = [];
const senderName = user.profile.name;
target.items.special.thankyouReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'thankyou',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
birthday: {
text: t('birthdayCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('birthdayCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.birthday) user.achievements.birthday = 0;
user.achievements.birthday += 1;
} else {
each([user, target], u => {
if (!u.achievements.birthday) u.achievements.birthday = 0;
u.achievements.birthday += 1;
});
}
if (!target.items.special.birthdayReceived) target.items.special.birthdayReceived = [];
const senderName = user.profile.name;
target.items.special.birthdayReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'birthday',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
congrats: {
text: t('congratsCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('congratsCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.congrats) user.achievements.congrats = 0;
user.achievements.congrats += 1;
} else {
each([user, target], u => {
if (!u.achievements.congrats) u.achievements.congrats = 0;
u.achievements.congrats += 1;
});
}
if (!target.items.special.congratsReceived) target.items.special.congratsReceived = [];
const senderName = user.profile.name;
target.items.special.congratsReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'congrats',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
getwell: {
text: t('getwellCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('getwellCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.getwell) user.achievements.getwell = 0;
user.achievements.getwell += 1;
} else {
each([user, target], u => {
if (!u.achievements.getwell) u.achievements.getwell = 0;
u.achievements.getwell += 1;
});
}
if (!target.items.special.getwellReceived) target.items.special.getwellReceived = [];
const senderName = user.profile.name;
target.items.special.getwellReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'getwell',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
goodluck: {
text: t('goodluckCard'),
mana: 0,
value: 10,
immediateUse: true,
silent: true,
target: 'user',
notes: t('goodluckCardNotes'),
cast (user, target) {
if (user === target) {
if (!user.achievements.goodluck) user.achievements.goodluck = 0;
user.achievements.goodluck += 1;
} else {
each([user, target], u => {
if (!u.achievements.goodluck) u.achievements.goodluck = 0;
u.achievements.goodluck += 1;
});
}
if (!target.items.special.goodluckReceived) target.items.special.goodluckReceived = [];
const senderName = user.profile.name;
target.items.special.goodluckReceived.push(senderName);
if (target.addNotification) {
target.addNotification('CARD_RECEIVED', {
card: 'goodluck',
from: {
id: user._id,
name: senderName,
},
});
}
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
};
each(spells, spellClass => {
each(spellClass, (spell, key) => {
spell.key = key;
const _cast = spell.cast;
spell.cast = function castSpell (user, target, req) {
_cast(user, target, req);
user.stats.mp -= spell.mana;
};
});
});
export default spells;