# Handles giving achievements to players # Due to time constraints, this was rushed and not very generalized. This module can be expanded # on in the future. {_} = require 'underscore' async = require 'async' config = require './config' redis = require './redis' ratings = require './ratings' {Conditions} = require '../shared/conditions' request = require 'request' authHeaders = {AUTHUSER: process.env.AUTHUSER, AUTHTOKEN: process.env.AUTHTOKEN} request = request.defaults(json: true, headers: authHeaders, timeout: 30 * 1000) ACHIEVEMENT_KEY = 'achievements' winCount = (count) -> inner = (ratioData, streakData) -> ratioData.win >= count return inner streak = (count) -> inner = (ratioData, streakData) -> streakData.streak >= count return inner ACHIEVEMENTS = [ { id: 1 name: "Taste of Victory" condition: "Won on PokeBattle for the first time" medium_image: "1_TasteOfVictory_50x50.png" conditionFn: winCount(1) }, { id: 2 name: "Centurian" name: "Centurion" condition: "Won 100 times" medium_image: "100_Centurian_50x50.png" conditionFn: winCount(100) }, { id: 3 name: "Jackpot!" condition: "Lucky 777 victories" medium_image: "777_Jackpot_50x50.png" conditionFn: winCount(777) }, { id: 4 name: "Ladder Monster" condition: "Won a beastly 2500 matches" medium_image: "2500_LadderMonster_50x50_1.png" conditionFn: winCount(2500) }, { id: 5 name: "Mile High Club" condition: "Won 5280 ladder matches" medium_image: "5280_MileHighClub_50x50.png" conditionFn: winCount(5280) }, { id: 6 name: "Better than Phil" condition: "More than 7086 victories" medium_image: "7087_BetterThanPhil_50x50.png" conditionFn: winCount(7087) }, { id: 7 name: "KAKAROT!" condition: "What? More than 9000 victories?!" medium_image: "9001_KAKAROT_50x50.png" conditionFn: winCount(9001) }, # ladder ranking achievements go here { id: 26 name: "On a Roll!" condition: "Won 5 consecutive ladder matches" medium_image: "5_OnARoll_50x50.png" conditionFn: streak(5) }, { id: 27 name: "Perfect Ten!" condition: "Won 10 consecutive ladder matches" medium_image: "10_PerfectTen_50x50.png" conditionFn: streak(10) }, { id: 28 name: "Incredible!" condition: "Won 15 consecutive ladder matches" medium_image: "15_Incredible_50x50.png" conditionFn: streak(15) }, { id: 29 name: "Unreal!" condition: "Won 20 consecutive ladder matches" medium_image: "20_Unreal_50x50.png" conditionFn: streak(20) }, { id: 30 name: "Impossible!" condition: "Won 30 consecutive ladder matches" medium_image: "30_Impossible_50x50.png" conditionFn: streak(30) }, { id: 31 name: "The One" condition: "You've won 50 in a row! Amazing!" medium_image: "50_TheOne_50x50.png" conditionFn: streak(50) } ] # Checks what achievements a player is eligible for # The achievements are then awarded to the player # Note: In the current implementation, playerId is actually a name @checkAndAwardAchievements = (server, playerId, ratingKey, next = ->) -> checkAchievements ratingKey, (err, achievements) -> return next(err) if err return next() if achievements.length == 0 filterEarned playerId, achievements, (err, achievements) -> return next(err) if err return next() if achievements.length == 0 notifyServer playerId, achievements, (err, byStatus) -> return next(err) if err # TODO: Handle errors, probably add them to some queue to retry # Currently its fine, as playing another battle will do a re-evalation # but in the future this may not be the case! # Flag all of the achievements that have been earned achievementsToFlag = _.union(byStatus.success, byStatus.duplicate) flagEarned(playerId, achievementsToFlag) if achievementsToFlag.length > 0 # for each new achievement, notify the user if said user is online if byStatus.success.length > 0 user = server.getUser(playerId) if user user.send('achievementsEarned', byStatus.success) next() # Registers a battle with the achievement system # If the battle is not eligible for achievements, it is ignored @registerBattle = (server, battle) -> return if battle.format != 'xy1000' return unless Conditions.RATED_BATTLE in battle.conditions battle.once 'ratingsUpdated', => for player in battle.players @checkAndAwardAchievements(server, player.id, player.ratingKey) # Returns the achievements a player is eligible for, including already earned ones checkAchievements = (ratingKey, next) -> ratings.getRatio ratingKey, (err, ratio) -> ratings.getStreak ratingKey, (err, streak) -> achievements = ACHIEVEMENTS.filter((o) -> o.conditionFn(ratio, streak)) # Send everything except the conditionFn attribute results = (_(a).omit('conditionFn') for a in achievements) next(null, results) # Removes the achievements that have already been earned from the list of achievements filterEarned = (playerId, achievements, next) -> ids = _(achievements).pluck('id') redis.hmget "#{ACHIEVEMENT_KEY}:#{playerId}", ids, (err, flagged) -> return next(err) if err filtered = achievements.filter((a, i) -> !flagged[i]) next(null, filtered) # Flags achievements that have been earned in redis so we don't bother the webservice with it flagEarned = (playerId, achievements, next) -> hash = {} hash[a.id] = true for a in achievements redis.hmset("#{ACHIEVEMENT_KEY}:#{playerId}", hash) # Notifies the server about achievements to add to the user # All achievements are separated by server result and passed to next # Note: In the current implementation, playerId is actually a name # If playerId is refactored to a numerical id, the server will need to be updated notifyServer = (playerId, achievements, next) -> achievementsByStatus = success: [] duplicate: [] error: [] if config.IS_LOCAL achievementsByStatus.success = achievements return next(null, achievementsByStatus) calls = for achievement in achievements do (achievement) -> (callback) -> request.post { url: "https://www.pokebattle.com/api/v1/achievements/" json: { user: playerId, achievement: achievement.name } }, (err, res, data) -> status = "success" status = "error" if err status = "duplicate" if data?.awarded achievementsByStatus[status].push(achievement) callback() async.parallel calls, (err, results) -> next(null, achievementsByStatus)