Files
habitica/website/common/script/fns/randomDrop.js
Matteo Pagliazzi d0bc0dbe49 Add API Call to bulk score tasks (#11389)
* Add new API call to complete multiple task scorings in one call

* Improve API response

* Improve saving process

* Improve handling for multiple tasks scored at once

* Handle challenge task errors better

* Improve check for alias

* Improve check for task scorings

* Fix merge errors

* make nodemon ignore content_cache

* Fix completing group tasks

* fix test

* fix tests (again)

* typo

* WIP(a11y): task modal updates

* fix(tasks): borders in modal

* fix(tasks): circley locks

* fix(task-modal): placeholders

* WIP(task-modal): disabled states, hide empty options, +/- restyle

* fix(task-modal): box shadows instead of borders, habit control pointer

* fix(task-modal): button states?

* fix(modal): tighten up layout, new spacing utils

* fix(tasks): more stylin

* fix(tasks): habit hovers

* fix(css): checklist labels, a11y colors

* fix(css): one more missed hover issue

* fix(css): lock Challenges, label fixes

* fix(css): scope input/textarea changes

* fix(style): task tweakies

* fix(style): more button fixage

* WIP(component): start select list story

* working example of a templated selectList

* fix(style): more button corrections

* fix(lint): EOL

* fix(buttons): factor btn-secondary to better override Bootstrap

* fix(styles): standardize more buttons

* wip: difficulty select - style fixes

* selectDifficulty works! 🎉 - fix styles

* change the dropdown-item sizes only for the selectList ones

* selectTranslatedArray

* changed many label margins

* more correct dropdown style

* fix(modals): button corrections

* input-group styling + datetime picker without today button

* Style/margins for "repeat every" - extract selectTag.vue

* working tag-selection / update - cleanup

* fix stories

* fix svg color on create modal (purple)

* fix task modal bottom padding

* correct dropdown shadow

* update dropdown-toggle caret size / color

* fixed checklist style

* sync checked state

* selectTag padding

* fix spacing between positive/negative streak inputs

* toggle-checkbox + fix some spacings

* disable repeat-on when its a groupTask

* fix new checklist-item

* fix toggle-checkbox style - fix difficulty style

* fix checklist ui

* add tags label , when there arent any tags selected

* WORKING select-tag component 🎉

* fix taglist story

* show max 5 items in tag dropdown + "X more" label

* fix datetime clear button

* replace m-b-xs to mb-1 (bootstrap) - fix input-group-text style

* fix styles of advanced settings

* fix delete task styles

* always show grippy on hover of the item

* extract modal-text-input mixin + fix the borders/dropshadow

* fix(spacing): revert most to Bootstrap

* feat(checklists): make local copy of master checklist non-editable
also aggressively update checklists because they weren't syncing??

* fix(checklists): handle add/remove options better

* feat(teams): manager notes field

* fix select/dropdown styles

* input border + icon colors

* delete task underline color

* fix checklist "delete icon" vertical position

* selectTag fixes - normal open/close toggle working again - remove icon color

* fixing icons:

Trash can - Delete
Little X - Remove
Big X - Close
Block - Block

* fix taglist margins / icon sizes

* wip margin overview (in storybook)

* fix routerlink

* remove unused method

* new selectTag style + add markdown inside tagList + scrollable tag selection

* fix selectTag / selectList active border

* fix difficulty select (svg default color)

* fix input padding-left + fix reset habit streak fullwidth / padding + "repeat every" gray text (no border)

* feat(teams): improved approval request > approve > reward flow

* fix(tests): address failures

* fix(lint): oops only

* fix(tasks): short-circuit group related logic

* fix(tasks): more short circuiting

* fix(tasks): more lines, less lint

* fix(tasks): how do i keep missing these

* feat(teams): provide assigning user summary

* fix(teams): don't attempt to record assiging user if not supplied

* fix advanced-settings styling / margin

* fix merge + hide advanced streak settings when none enabled

* fix styles

* set Roboto font for advanced settings

* Add Challenge flag to the tag list

* add tag with enter, when no other tag is found

* fix styles + tag cancel button

* refactor footer / margin

* split repeat fields into option mt-3 groups

* button all the things

* fix(tasks): style updates
* no hover state for non-editable tasks on team board
* keep assign/claim footer on task after requesting approval
* disable more fields on user copy of team task, and remove hover states 
for them

* fix(tasks): functional revisions
* "Claim Rewards" instead of "x" in task approved notif
* Remove default transition supplied by Bootstrap, apply individually to 
some elements
* Delete individual tasks and related notifications when master task 
deleted from team board
* Manager notes now save when supplied at task initial creation
* Can no longer dismiss rewards from approved task by hitting Dismiss 
All

* fix(tasks): clean tasksOrder
also adjust related test expectation

* fix(tests): adjust integration expectations

* fix(test): ratzen fratzen only

* fix lint

* fix tests

* fix(teams): checklist, notes

* handleSharedCompletion: handle error, make sure it is run after the user task has been saved

* fix typo

* correctly handle errors in handleSharedCompletion when approving a task

* fix(teams): improve disabled states

* handleSharedCompletion: do not increase completions by 1 manually to adjust for last approval not saved yet

* revert changes to config.json.example

* fix(teams): more style fixage

* add unit tests for findMultipleByIdOrAlias

* exclude api v4 route from apidocs

* BREAKING(teams): return 202 instead of 401 for approval request

* fix(teams): better taskboard sync
also re-re-fix checklist borders

* scoreTasks: validate body

* fix tests, move string to api errors

* fix(tests): update expectations for breaking change

* start updating api docs, process tasks sequentially to avoid conflicts with user._tmp

* do not crash entire bulk operation in case of errors

* save task only if modified

* fix lint

* undo changes to error handling: either all tasks scoring are successfull or none

* remove stale code

* do not return user._tmp when bulk scoring, it would be the last version only

* make sure user._tmp.leveledUp is not lost when bulk scoring

* rewards tests

* mixed tests

* fix tests, allow scoring the same task multiple times

* finish integration tests

* fix api docs for the bulk score route

* refactor(task-modal): lockable label component

* wip loading spinner

* refactor(teams): move task scoring to mixin

* fix(teams): style corrections

* fix(btn): fix padding to have height of 32px

* implement loading spinner

* remove console.log warnings

* fix(tasks): spacing and wording corrections

* fix(teams): don't bork manager notes

* fix(teams): assignment fix and more approval flow revisions

* WIP(teams): use tag dropdown control for assignment

* finish merge - never throw an error when a group task requires approval (wip - needs tests)

* fix taskModal merge

* fix merge

* fix(task modal): add newline

* fix(column.vue): add newline at end of file

* mvp yesterdaily modal

* fix tests

* fix api docs for bulk scoring group tasks

* separate task scoring and _tmp handling

* handle _tmp when bulk scoring

* rya: close modal before calling cron API, prevents issues with modals

* rya: fix conflicts with other modals

* add sounds, support for group plans, analytics

* use asyncResource for group plans

* fix lint

* streak bonus: add comment about missing in rya

* move yesterdailyModal

* fix issues with level up modals and rya

* add comments for future use, fix level up modals not showing up at levels with a quest drop

* handle errors in rya modal

* bundle quest and crit notifications

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Phillip Thelen <viirus@pherth.net>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: negue <eugen.bolz@gmail.com>
2020-08-21 11:46:56 +02:00

179 lines
6.1 KiB
JavaScript

import cloneDeepWith from 'lodash/cloneDeepWith';
import isFunction from 'lodash/isFunction';
import min from 'lodash/min';
import reduce from 'lodash/reduce';
import filter from 'lodash/filter';
import pickBy from 'lodash/pickBy';
import size from 'lodash/size';
import moment from 'moment';
import content from '../content/index';
import i18n from '../i18n';
import { daysSince } from '../cron';
import { diminishingReturns } from '../statHelpers';
import randomVal from '../libs/randomVal';
import statsComputed from '../libs/statsComputed';
import firstDrops from './firstDrops';
// TODO This is only used on the server
// move to user model as an instance method?
// Clone a drop object maintaining its functions
// so that we can change it without affecting the original item
function cloneDropItem (drop) {
return cloneDeepWith(drop, val => (isFunction(val) ? val : undefined));
}
function trueRandom () {
return Math.random();
}
export default function randomDrop (user, options, req = {}, analytics) {
let acceptableDrops;
let drop;
let dropMultiplier;
let rarity;
if (
size(user.items.eggs) < 1
&& size(user.items.hatchingPotions) < 1
) {
// @TODO why are we using both _tmp.firstDrops and the FIRST_DROPS notification?
user._tmp.firstDrops = firstDrops(user);
return;
}
const predictableRandom = options.predictableRandom || trueRandom;
const { task } = options;
let chance = min([Math.abs(task.value - 21.27), 37.5]) / 150 + 0.02;
chance *= task.priority // Task priority: +50% for Medium, +100% for Hard
// start users with +75% drops, diminishing by 5% per level gained
* (1 + Math.max(0, 80 - (5 * user.stats.lvl)) / 100)
* (1 + (task.streak / 100 || 0)) // Streak bonus: +1% per streak
* (1 + statsComputed(user).per / 100) // PERception: +1% per point
* (1 + (user.contributor.level / 40 || 0)) // Contrib levels: +2.5% per level
* (1 + (user.achievements.rebirths / 20 || 0)) // Rebirths: +5% per achievement
* (1 + (user.achievements.streak / 200 || 0)) // Streak achievements: +0.5% per achievement
// +50% per checklist item complete. TODO: make this into X individual drop chances instead
* (user._tmp.crit || 1)
* (1 + 0.5 * (reduce(
task.checklist, (m, i) => m + (i.completed ? 1 : 0), // eslint-disable-line indent
0,
) || 0)); // eslint-disable-line indent
chance = diminishingReturns(chance, 0.75);
if (predictableRandom() < chance) {
user.party.quest.progress.collectedItems = user.party.quest.progress.collectedItems || 0;
user.party.quest.progress.collectedItems += 1;
user._tmp.quest = user._tmp.quest || {};
user._tmp.quest.collection = 1;
if (user.markModified) user.markModified('party.quest.progress');
}
if (user.purchased && user.purchased.plan && user.purchased.plan.customerId) {
dropMultiplier = 2;
} else {
dropMultiplier = 1;
}
const maxDropCount = dropMultiplier * (5 + Math.floor(statsComputed(user).per / 25) + (user.contributor.level || 0)); // eslint-disable-line max-len
if (
daysSince(user.items.lastDrop.date, user.preferences) === 0
&& user.items.lastDrop.count >= maxDropCount
) {
return;
}
const firstFoodDrop = size(user.items.food) < 1;
if (firstFoodDrop || predictableRandom() < chance) {
rarity = predictableRandom();
if (firstFoodDrop || rarity > 0.6) { // food 40% chance
drop = cloneDropItem(randomVal(filter(content.food, {
canDrop: true,
})));
user.items.food = {
...user.items.food,
[drop.key]: user.items.food[drop.key] || 0,
};
user.items.food[drop.key] += 1;
if (user.markModified) user.markModified('items.food');
drop.type = 'Food';
drop.dialog = i18n.t('messageDropFood', {
dropText: drop.textA(req.language),
dropNotes: drop.notes(req.language),
}, req.language);
} else if (rarity > 0.3) { // eggs 30% chance
drop = cloneDropItem(randomVal(content.dropEggs));
user.items.eggs = {
...user.items.eggs,
[drop.key]: user.items.eggs[drop.key] || 0,
};
user.items.eggs[drop.key] += 1;
if (user.markModified) user.markModified('items.eggs');
drop.type = 'Egg';
drop.dialog = i18n.t('messageDropEgg', {
dropText: drop.text(req.language),
dropNotes: drop.notes(req.language),
}, req.language);
} else { // Hatching Potion, 30% chance - break down by rarity.
if (rarity < 0.02) { // Very Rare: 10% (of 30%)
acceptableDrops = ['Golden'];
} else if (rarity < 0.09) { // Rare: 20% of 30%
acceptableDrops = ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'];
} else if (rarity < 0.18) { // uncommon: 30% of 30%
acceptableDrops = ['Red', 'Shade', 'Skeleton'];
} else { // common, 40% of 30%
acceptableDrops = ['Base', 'White', 'Desert'];
}
drop = cloneDropItem(
randomVal(pickBy(content.hatchingPotions, (v, k) => acceptableDrops.indexOf(k) >= 0)),
);
user.items.hatchingPotions = {
...user.items.hatchingPotions,
[drop.key]: user.items.hatchingPotions[drop.key] || 0,
};
user.items.hatchingPotions[drop.key] += 1;
if (user.markModified) user.markModified('items.hatchingPotions');
drop.type = 'HatchingPotion';
drop.dialog = i18n.t('messageDropPotion', {
dropText: drop.text(req.language),
dropNotes: drop.notes(req.language),
}, req.language);
}
// @TODO use notifications
user._tmp.drop = drop;
user.items.lastDrop.date = Number(new Date());
user.items.lastDrop.count += 1;
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('dropped item', {
uuid: user._id,
itemKey: drop.key,
acquireMethod: 'Drop',
category: 'behavior',
headers: req.headers,
});
if (user.items.lastDrop.count === maxDropCount) {
analytics.track('drop cap reached', {
uuid: user._id,
dropCap: maxDropCount,
category: 'behavior',
headers: req.headers,
});
}
}
}
}