Newer
Older
* Unlock achievements and earn points based on milestones.
*/
/**
* Implements hook_permission().
function achievements_permission() {
return array(
'access achievements' => array(
'title' => t('Access achievements'),
),
'earn achievements' => array(
'title' => t('Earn achievements'),
),
'administer achievements' => array(
'title' => t('Administer achievements'),
),
);
}
*/
function achievements_menu() {
$items['achievements/leaderboard'] = array(
'access arguments' => array('access achievements'),
'description' => 'View the site-wide achievements leaderboard.',
'file' => 'achievements.pages.inc',
'page callback' => 'achievements_leaderboard_totals',
'title' => 'Leaderboard',
);
$items['achievements/leaderboard/%achievements'] = array(
'access arguments' => array('access achievements'),
'description' => "View a specific achievement's leaderboard.",
'file' => 'achievements.pages.inc',
'page callback' => 'achievements_leaderboard_for',
'page arguments' => array(2),
'title' => 'Per-achievement leaderboard',
'type' => MENU_CALLBACK,
);
$items['user/%user/achievements'] = array(
'access arguments' => array('access achievements'),
'description' => "View a specific user's leaderboard.",
'file' => 'achievements.pages.inc',
'page callback' => 'achievements_user_page',
'page arguments' => array(1),
'title' => 'Achievements',
'type' => MENU_LOCAL_TASK,
);
$items['admin/config/people/achievements'] = array(
'description' => 'Configure the achievements system.',
'page callback' => 'drupal_get_form',
'page arguments' => array('achievements_settings'),
'title' => 'Achievements',
);
$items['achievements/autocomplete'] = array(
'access arguments' => array('access achievements'),
'file' => 'achievements.pages.inc',
'page callback' => 'achievements_autocomplete',
'title' => 'Achievement title autocomplete',
'type' => MENU_CALLBACK,
);
function achievements_theme() {
return array(
'achievement' => array(
'variables' => array('achievement' => NULL, 'unlock' => NULL),
'template' => 'achievement',
),
'achievement_notification' => array(
'variables' => array('achievement' => NULL, 'unlock' => NULL),
'template' => 'achievement-notification',
),
/**
* Process variables for achievement.tpl.php.
*/
function template_preprocess_achievement(&$variables) {
achievements_template_shared_variables($variables);
}
/**
* Process variables for achievement-notification.tpl.php.
*/
function template_preprocess_achievement_notification(&$variables) {
achievements_template_shared_variables($variables);
$variables['classes_array'][] = 'element-hidden';
}
/**
* Standard variables used in our achievement templates.
*
* All our achievement templates send in $achievement and $unlock, but display
* some or all of the data in different ways. This is a centralized collection
* of the various helper $variables needed for theme display.
*/
function achievements_template_shared_variables(&$variables) {
$variables['state'] = isset($variables['unlock']) ? 'unlocked' : 'locked';
if (isset($variables['achievement']['hidden']) && !achievements_unlocked_already($variables['achievement']['id'])) {
$variables['achievement']['points'] = t('???'); // IIiII haveEeeA aa seecFRrit and I'll NEEVvaaha hTellLL..
$variables['achievement']['title'] = t('Hidden achievement'); // unless, of course, you PayPal me bribes.
$variables['achievement']['description'] = t('Continue playing to unlock this hidden achievement.');
$variables['state'] = 'hidden';
}
$variables['classes_array'][] = 'achievement-' . $variables['state'];
$variables['classes_array'][] = 'ui-corner-all'; // if jquery UI is enabled, adds rounded rects.
$variables['achievement_url'] = url('achievements/leaderboard/' . $variables['achievement']['id']);
$variables['unlocked_date'] = isset($variables['unlock']['timestamp']) ? format_date($variables['unlock']['timestamp'], 'custom', 'Y/m/d') : '';
$variables['unlocked_rank'] = isset($variables['unlock']['rank']) ? t('Rank #@rank', array('@rank' => $variables['unlock']['rank'])) : '';
// set the per-achievement image or admin default.
$default = drupal_get_path('module', 'achievements') . '/images/default-' . $variables['state'] . '-70.jpg';
$variables['image'] = isset($variables['achievement']['images'][$variables['state']])
? theme('image', array('path' => $variables['achievement']['images'][$variables['state']]))
: theme('image', array('path' => variable_get('achievements_image_' . $variables['state'], $default)));
$variables['image'] = l($variables['image'], 'achievements/leaderboard/' . $variables['achievement']['id'], array('html' => TRUE));
'achievements_leaderboard' => array(
'info' => t('Achievements leaderboard'),
'cache' => DRUPAL_CACHE_GLOBAL,
function achievements_block_view($delta = '') {
if ($delta == 'achievements_leaderboard') {
include_once(drupal_get_path('module', 'achievements') . '/achievements.pages.inc');
return array( // stupid file that I have to include. WHERE"S MY FUNCTION REGSITERYR.
'content' => achievements_leaderboard_block(),
/**
* Implements hook_block_configure().
*/
function achievements_block_configure($delta = '') {
if ($delta == 'achievements_leaderboard') {
$form['achievements_rankings'] = array(
'#description' => t('Enabling the "relative leaderboard" will show the current user\'s position and, optionally, a number of ranks before and after that position. For example, if the current user is ranked 12th and you configure 3 nearby ranks, the relative leaderboard will show ranks 9 through 15. The relative leaderboard will only show if the logged-in user does not appear in the displayed top ranks.'),
'#title' => t('Leaderboard ranks'),
'#type' => 'fieldset',
);
$form['achievements_rankings']['achievements_leaderboard_block_count_top'] = array(
'#type' => 'select',
'#title' => t('Number of top ranks'),
'#default_value' => variable_get('achievements_leaderboard_block_count_top', 5),
'#options' => drupal_map_assoc(range(0, 30)),
);
$form['achievements_rankings']['achievements_leaderboard_block_relative'] = array(
'#type' => 'radios',
'#title' => t('Relative leaderboard display'),
'#default_value' => variable_get('achievements_leaderboard_block_relative', 'nearby_ranks'),
'#options' => array(
'disabled' => t('Don\'t show the relative leaderboard'),
'user_only' => t('Show only the current user'),
'nearby_ranks' => t('Show the current user and nearby ranks'),
),
);
$form['achievements_rankings']['achievements_leaderboard_block_relative_nearby_ranks'] = array(
'#type' => 'select',
'#title' => t('Number of nearby ranks to display'),
'#default_value' => variable_get('achievements_leaderboard_block_relative_nearby_ranks', 1),
'#options' => drupal_map_assoc(range(1, 10)),
);
return $form;
}
}
/**
* Implements hook_block_save().
*/
function achievements_block_save($delta = '', $edit = array()) {
if ($delta == 'achievements_leaderboard') { // bleh, this is wasteful code that should be automated.
variable_set('achievements_leaderboard_block_count_top', $edit['achievements_leaderboard_block_count_top']);
variable_set('achievements_leaderboard_block_relative', $edit['achievements_leaderboard_block_relative']);
variable_set('achievements_leaderboard_block_relative_nearby_ranks', $edit['achievements_leaderboard_block_relative_nearby_ranks']);
}
}
/**
* Load information about our achievements.
*
* @param $achievement_id
* The (optional) achievement this request applies against.
Morbus Iff
committed
* @param $grouped
* Whether to return the achievements list flattened (FALSE, the default)
* or grouped into achievement-defined categories. If TRUE, but there is no
* group specified for an achievement, it'll be stored in a "-none-" array
Morbus Iff
committed
* intended to simplify display code. Not compatible with $achievement_id.
* @return $achievements
* An array of all achievements, or just the one passed.
*/
Morbus Iff
committed
function achievements_load($achievement_id = NULL, $grouped = FALSE, $reset = FALSE) {
$achievements = &drupal_static(__FUNCTION__);
Morbus Iff
committed
if (!$reset && $cache = cache_get('achievements_info')) {
$achievements = array('flat' => array(), 'grouped' => array());
Morbus Iff
committed
$result = module_invoke_all('achievements_info');
Morbus Iff
committed
drupal_alter('achievements_info', $result);
Morbus Iff
committed
// determine if we're looking at an achievement or group and create our
// master $achievements array. we store the achievements in two separate
// forms: one with a tree (for display purposes) and one flattened (for
// lookup purposes). the flattened index is referenced so that we save
// space in the final serialized blob that cache_set() sends.
foreach ($result as $key => $value) {
if (isset($value['achievements']) && is_array($value['achievements'])) {
$achievements['grouped'][$key] = $value; // copy the whole shebang into realz.
foreach ($achievements['grouped'][$key]['achievements'] as $id => $achievement) {
$achievements['grouped'][$key]['achievements'][$id]['id'] = $id;
$achievements['grouped'][$key]['achievements'][$id]['group_id'] = $key;
$achievements['grouped'][$key]['achievements'][$id]['group_title'] = $value['title'];
Morbus Iff
committed
$achievements['flat'][$id] = &$achievements['grouped'][$key]['achievements'][$id];
}
}
else {
$value['id'] = $key;
$value['group_id'] = '-none-';
Morbus Iff
committed
$value['group_title'] = NULL; // moo.
$achievements['grouped']['-none-']['achievements'][$key] = $value;
$achievements['flat'][$key] = &$achievements['grouped']['-none-']['achievements'][$key];
Morbus Iff
committed
}
if (isset($achievements['grouped']['-none-'])) {
$achievements['grouped']['-none-']['title'] = t('Miscellany');
}
Morbus Iff
committed
cache_set('achievements_info', $achievements, 'cache', CACHE_TEMPORARY);
Morbus Iff
committed
if ($achievement_id) { // all my majesty and brilliance, and you just want one result? /me weeps.
return isset($achievements['flat'][$achievement_id]) ? $achievements['flat'][$achievement_id] : FALSE;
}
// return the whole shebang in groups or a flattened lookup bucket.
return $grouped ? $achievements['grouped'] : $achievements['flat'];
* Returns a user-centric leaderboard.
* @param $nearby
* How many nearby ranks/users should be returned (defaults to 0).
Morbus Iff
committed
* @param $uid
* The user to return achievement info for (defaults to current user).
*
Morbus Iff
committed
* @return $results
* Either an empty array (for users who have yet to unlock anything) or
* or an array of user(s) and their achievement statistics, keyed to uid.
Morbus Iff
committed
function achievements_totals_user($nearby = 0, $uid = NULL) {
list($uid, $access) = achievements_user_is_achiever($uid);
// we don't check for access as this is info grubbing only.
$achievers = &drupal_static(__FUNCTION__);
$query = db_select('achievement_totals', 'at');
$query->addTag('achievement_totals_user')->join('users', 'u', 'u.uid = at.uid');
$query->fields('at', array('uid', 'points', 'unlocks', 'timestamp', 'achievement_id'))->fields('u', array('name'));
$achievers[$uid] = $query->condition('at.uid', $uid)->execute()->fetchObject();
if (isset($achievers[$uid]->points)) { // only if they've unlocked something.
// to find the user's rank: count all the users with greater points, add
// all the users with equal points but earlier timestamps, and then add 1.
$better_points = db_select('achievement_totals')->condition('points', $achievers[$uid]->points, '>')->countQuery()->execute()->fetchField();
$earlier_times = db_select('achievement_totals')->condition('points', $achievers[$uid]->points)->condition('timestamp', $achievers[$uid]->timestamp, '<')->countQuery()->execute()->fetchField();
$achievers[$uid]->rank = $better_points + $earlier_times + 1;
}
// we need to get some nearby users, so use our global leaderboard query to
// get a range based on the $uid's current rank and how many $nearby users.
if (isset($achievers[$uid]->points) && $nearby) {
$starting_rank = max(1, $achievers[$uid]->rank - $nearby);
// build the magical rank-ranging query.
$query = db_select('achievement_totals', 'at');
$query->addTag('achievement_totals_user_nearby')->join('users', 'u', 'u.uid = at.uid');
$query->fields('at', array('uid', 'points', 'unlocks', 'timestamp', 'achievement_id'))->fields('u', array('name'));
$query->orderBy('at.points', 'DESC')->orderBy('at.timestamp'); // @bug DESC/ASC doesn't index. at all. curses.
$offset = max(0, $achievers[$uid]->rank - $nearby - 1); // max() ensures no negative numbers in our lookup.
$limit = $achievers[$uid]->rank - $offset + $nearby; // always get +$nearby from current rank.
$achievers = $query->range($offset, $limit)->execute()->fetchAllAssoc('uid');
foreach ($achievers as $achiever) {
$achiever->rank = $starting_rank++;
}
}
return isset($achievers[$uid]->points) ? $achievers : array();
}
/**
* Logs a user as having unlocked an achievement.
*
* @param $achievement_id
* The achievement this request applies against.
* @param $uid
* The user to unlock an achievement for (defaults to current user).
*/
function achievements_unlocked($achievement_id, $uid = NULL) {
list($uid, $access) = achievements_user_is_achiever($uid);
if (!$access) { return; } // i know you want it, but...
// grab information about the achievement.
$achievement = achievements_load($achievement_id);
if (!isset($achievement)) { // hrm... try a cache refresh?
Morbus Iff
committed
$achievement = achievements_load($achievement_id, FALSE, TRUE);
if (isset($achievement) && !achievements_unlocked_already($achievement_id, $uid)) {
$last_rank = db_select('achievement_unlocks', 'au')->fields('au', array('rank')) // not. exciting. at. all.
->condition('achievement_id', $achievement_id)->orderBy('rank', 'DESC')->range(0, 1)->execute()->fetchField();
db_insert('achievement_unlocks')
->fields(array(
'achievement_id' => $achievement_id,
'uid' => $uid,
'rank' => $last_rank ? $last_rank + 1 : 1,
'timestamp' => REQUEST_TIME,
'seen' => 0,
))
->execute();
db_merge('achievement_totals')
->key(array('uid' => $uid))
->fields(array(
'points' => $achievement['points'],
'unlocks' => 1, // OMG CONGRATS
'timestamp' => REQUEST_TIME,
'achievement_id' => $achievement_id,
))
->expression('points', 'points + :points', array(':points' => $achievement['points']))
->expression('unlocks', 'unlocks + :increment', array(':increment' => 1))
->execute();
Morbus Iff
committed
module_invoke_all('achievements_unlocked', $achievement, $uid);
watchdog('achievements', t('Unlocked: %achievement (+@points).'),
array('%achievement' => $achievement['title'], '@points' => $achievement['points']),
WATCHDOG_NOTICE, l(t('view'), 'user/' . $uid . '/achievements')); // nothing fancy.
Morbus Iff
committed
* Return data about a user's unlocked achievements.
Morbus Iff
committed
* A specific achievement to check the unlock status of.
* The user this request applies against (defaults to current user).
Morbus Iff
committed
* @return NULL or $unlocked or $unlocks
* One of the following, based on the passed parameters:
* - If the $uid has not unlocked $achievement_id, return NULL.
* - If $achievement_id is unlocked, return an array of rank and timestamp.
* - If no $achievement_id is passed, an array of all $uid's unlocks.
Morbus Iff
committed
function achievements_unlocked_already($achievement_id = NULL, $uid = NULL) {
list($uid, $access) = achievements_user_is_achiever($uid);
if (!$access) { return; } // i can't let you in, y'know?
Morbus Iff
committed
$unlocks = &drupal_static(__FUNCTION__);
Morbus Iff
committed
if (!isset($unlocks[$uid])) {
// we grab all unlocks and cache per page load. it's better than lots of per-achievement lookups.
$unlocks[$uid] = db_select('achievement_unlocks', 'au')->fields('au', array('achievement_id', 'rank', 'timestamp'))
Morbus Iff
committed
->condition('uid', $uid)->execute()->fetchAllAssoc('achievement_id', PDO::FETCH_ASSOC); // INSERT FORTUNE, LONELINESS.
Morbus Iff
committed
}
Morbus Iff
committed
if (isset($achievement_id)) { // return data about a specific unlock if requested.
return isset($unlocks[$uid][$achievement_id]) ? $unlocks[$uid][$achievement_id] : NULL;
}
else { // all of 'em.
return $unlocks[$uid];
}
}
/**
* Retrieve data needed by an achievement.
*
* @param $achievement_id
* An identifier for the achievement whose data is being collected.
* @param $uid
* The user this stored data applies to (defaults to current user).
* The data stored for this achievement and user (unserialized).
*/
function achievements_storage_get($achievement_id = NULL, $uid = NULL) {
list($uid, $access) = achievements_user_is_achiever($uid);
if (!$access) { return; } // it's not that I don't want to...
return unserialize(db_select('achievement_storage')->fields('achievement_storage', array('data'))
->condition('achievement_id', $achievement_id)->condition('uid', $uid)->execute()->fetchField());
}
/**
* Save data needed by an achievement.
*
* @param $achievement_id
* An identifier for the achievement whose data is being collected.
* @param $uid
* The user this stored data applies to (defaults to current user).
* @param $data
* The data being saved (of any type; serialization occurs).
*/
function achievements_storage_set($achievement_id = NULL, $data = NULL, $uid = NULL) {
list($uid, $access) = achievements_user_is_achiever($uid);
if (!$access) { return; } // I... I'M IN LOVE WITH MORBUS OK?!!?
db_merge('achievement_storage')
->key(array('uid' => $uid, 'achievement_id' => $achievement_id))
->fields(array('data' => serialize($data))) // it's hot in here.
->execute(); // i hate all DBTNG syntax. NEVAH STANDARNDIZEE?Ee1
/**
* Determine if a user is able to earn achievements.
*
* This is a general helper around the core achievements functions and allows
* us to default to the global user if a $uid is not passed, but also check
* permissions against a user who is not the global user. This allows us to
* a) define roles of users that can not earn achievements and b) manually
* unlock achievements for a non-current user.
*
* @param $uid
* The user to check for "earn achievements" (defaults to current user).
*
* @return $results
* An array with values of:
* - $uid is the determined user (default: the global user).
* - $access is a TRUE or FALSE as returned by user_access().
*/
function achievements_user_is_achiever($uid = NULL) {
if (!isset($uid) || $uid == $GLOBALS['user']->uid) {
return array($GLOBALS['user']->uid, user_access('earn achievements'));
}
else {
return array($uid, user_access('earn achievements', user_load($uid)));
}
}
function achievements_user_cancel($edit, $account, $method) {
achievements_user_delete($account); // no stats for non-players.
function achievements_user_delete($account) {
db_delete('achievement_totals')->condition('uid', $account->uid)->execute();
db_delete('achievement_unlocks')->condition('uid', $account->uid)->execute();
db_delete('achievement_storage')->condition('uid', $account->uid)->execute();
/**
* Implements hook_page_alter().
*/
function achievements_page_alter(&$page) {
if (achievements_user_is_achiever()) {
$unlocks = db_select('achievement_unlocks', 'au')->fields('au', array('achievement_id', 'rank', 'timestamp')) // find everything we've yet to see.
->condition('uid', $GLOBALS['user']->uid)->condition('seen', 0)->orderBy('timestamp')->execute()->fetchAllAssoc('achievement_id', PDO::FETCH_ASSOC);
// if unseen unlocks are available, load in our JS libraries,
// display our achievement notification, and flag 'em as seen.
if (count($unlocks)) {
drupal_add_library('system', 'ui.dialog');
drupal_add_library('system', 'effects.fade');
drupal_add_js(drupal_get_path('module', 'achievements') . '/achievements.js');
foreach ($unlocks as $unlock) {
$achievement = achievements_load($unlock['achievement_id']);
$page['page_bottom']['achievements'][$unlock['achievement_id']] = array(
'#theme' => 'achievement_notification',
'#achievement' => $achievement,
Morbus Iff
committed
'#unlock' => $unlock,