BattleSim/server/achievements.coffee

220 lines
6.6 KiB
CoffeeScript

# 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)