376 lines
13 KiB
CoffeeScript
376 lines
13 KiB
CoffeeScript
{_} = require('underscore')
|
|
{Conditions} = require('../shared/conditions')
|
|
{Tiers} = require('../shared/tier')
|
|
{Protocol} = require('../shared/protocol')
|
|
pbv = require('../shared/pokebattle_values')
|
|
tiering = require('../shared/tier')
|
|
gen = require('./generations')
|
|
alts = require('./alts')
|
|
|
|
ConditionHash = {}
|
|
|
|
createCondition = (condition, effects = {}) ->
|
|
ConditionHash[condition] = effects
|
|
|
|
# Attaches each condition to the Battle facade.
|
|
@attach = (battleFacade) ->
|
|
battle = battleFacade.battle
|
|
for condition in battle.conditions
|
|
if condition not of ConditionHash
|
|
throw new Error("Undefined condition: #{condition}")
|
|
hash = ConditionHash[condition] || {}
|
|
# Attach each condition's event listeners
|
|
for eventName, callback of hash.attach
|
|
battle.on(eventName, callback)
|
|
|
|
# Extend battle with each function
|
|
# TODO: Attach to prototype, and only once.
|
|
for funcName, funcRef of hash.extend
|
|
battle[funcName] = funcRef
|
|
|
|
for funcName, funcRef of hash.extendFacade
|
|
battleFacade[funcName] = funcRef
|
|
|
|
# validates an entire team
|
|
@validateTeam = (conditions, team, genData) ->
|
|
errors = []
|
|
for condition in conditions
|
|
if condition not of ConditionHash
|
|
throw new Error("Undefined condition: #{condition}")
|
|
validator = ConditionHash[condition].validateTeam
|
|
continue if !validator
|
|
errors.push(validator(team, genData)...)
|
|
return errors
|
|
|
|
# validates a single pokemon
|
|
@validatePokemon = (conditions, pokemon, genData, prefix) ->
|
|
errors = []
|
|
for condition in conditions
|
|
if condition not of ConditionHash
|
|
throw new Error("Undefined condition: #{condition}")
|
|
validator = ConditionHash[condition].validatePokemon
|
|
continue if !validator
|
|
errors.push(validator(pokemon, genData, prefix)...)
|
|
return errors
|
|
|
|
createPBVCondition = (totalPBV) ->
|
|
createCondition Conditions["PBV_#{totalPBV}"],
|
|
validateTeam: (team, genData) ->
|
|
errors = []
|
|
if pbv.determinePBV(genData, team) > totalPBV
|
|
errors.push "Total team PBV cannot surpass #{totalPBV}."
|
|
if team.length != 6
|
|
errors.push "Your team must have 6 pokemon."
|
|
return errors
|
|
|
|
validatePokemon: (pokemon, genData, prefix) ->
|
|
errors = []
|
|
MAX_INDIVIDUAL_PBV = Math.floor(totalPBV / 3)
|
|
individualPBV = pbv.determinePBV(genData, pokemon)
|
|
|
|
if individualPBV > MAX_INDIVIDUAL_PBV
|
|
errors.push "#{prefix}: This Pokemon's PBV is #{individualPBV}. Individual
|
|
PBVs cannot go over 1/3 the total (over #{MAX_INDIVIDUAL_PBV} PBV)."
|
|
|
|
return errors
|
|
|
|
createPBVCondition(1000)
|
|
createPBVCondition(500)
|
|
|
|
createTierCondition = (conditionName, tier) ->
|
|
createCondition Conditions[conditionName],
|
|
validateTeam: (team, genData) ->
|
|
errors = []
|
|
tierdata = Tiers[tier]
|
|
teamtier = tiering.determineTier(genData, team)
|
|
if teamtier.tierRank > tierdata.tierRank
|
|
errors.push "Your team tier may not exceed the #{tierdata.humanName} tier"
|
|
if team.length != 6
|
|
errors.push "Your team must have 6 pokemon."
|
|
return errors
|
|
|
|
for key, val of Tiers
|
|
createTierCondition("TIER_#{key}", key)
|
|
|
|
createCondition Conditions.SLEEP_CLAUSE,
|
|
attach:
|
|
initialize: ->
|
|
for team in @getTeams()
|
|
for p in team.pokemon
|
|
p.attach(@getAttachment("SleepClause"))
|
|
|
|
createCondition Conditions.SPECIES_CLAUSE,
|
|
validateTeam: (team, genData) ->
|
|
errors = []
|
|
species = team.map((p) -> p.species)
|
|
species.sort()
|
|
for i in [1...species.length]
|
|
speciesName = species[i - 1]
|
|
if speciesName == species[i]
|
|
errors.push("Cannot have the same species: #{speciesName}")
|
|
while speciesName == species[i]
|
|
i++
|
|
return errors
|
|
|
|
createCondition Conditions.EVASION_CLAUSE,
|
|
validatePokemon: (pokemon, genData, prefix) ->
|
|
{moves, ability} = pokemon
|
|
errors = []
|
|
|
|
# Check evasion abilities
|
|
if ability in [ "Moody" ]
|
|
errors.push("#{prefix}: #{ability} is banned under Evasion Clause.")
|
|
|
|
# Check evasion moves
|
|
for moveName in moves || []
|
|
move = genData.MoveData[moveName]
|
|
continue if !move
|
|
if move.primaryBoostStats? && move.primaryBoostStats.evasion > 0 &&
|
|
move.primaryBoostTarget == 'self'
|
|
errors.push("#{prefix}: #{moveName} is banned under Evasion Clause.")
|
|
|
|
return errors
|
|
|
|
createCondition Conditions.OHKO_CLAUSE,
|
|
validatePokemon: (pokemon, genData, prefix) ->
|
|
{moves} = pokemon
|
|
errors = []
|
|
|
|
# Check OHKO moves
|
|
for moveName in moves || []
|
|
move = genData.MoveData[moveName]
|
|
continue if !move
|
|
if "ohko" in move.flags
|
|
errors.push("#{prefix}: #{moveName} is banned under One-Hit KO Clause.")
|
|
|
|
return errors
|
|
|
|
createCondition Conditions.PRANKSTER_SWAGGER_CLAUSE,
|
|
validatePokemon: (pokemon, genData, prefix) ->
|
|
errors = []
|
|
if "Swagger" in pokemon.moves && "Prankster" == pokemon.ability
|
|
errors.push("#{prefix}: A Pokemon can't have both Prankster and Swagger.")
|
|
return errors
|
|
|
|
createCondition Conditions.UNRELEASED_BAN,
|
|
validatePokemon: (pokemon, genData, prefix) ->
|
|
# Check for unreleased items
|
|
errors = []
|
|
if pokemon.item && genData.ItemData[pokemon.item]?.unreleased
|
|
errors.push("#{prefix}: The item '#{pokemon.item}' is unreleased.")
|
|
# Check for unreleased abilities
|
|
forme = genData.FormeData[pokemon.species][pokemon.forme || "default"]
|
|
if forme.unreleasedHidden && pokemon.ability == forme.hiddenAbility &&
|
|
forme.hiddenAbility not in forme.abilities
|
|
errors.push("#{prefix}: The ability #{pokemon.ability} is unreleased.")
|
|
# Check for unreleased Pokemon
|
|
if forme.unreleased
|
|
errors.push("#{prefix}: The Pokemon #{pokemon.species} is unreleased.")
|
|
return errors
|
|
|
|
createCondition Conditions.RATED_BATTLE,
|
|
attach:
|
|
end: (winnerId) ->
|
|
return if !winnerId
|
|
index = @getPlayerIndex(winnerId)
|
|
loserId = @playerIds[1 - index]
|
|
ratings = require './ratings'
|
|
|
|
winner = @getPlayer(winnerId)
|
|
loser = @getPlayer(loserId)
|
|
|
|
winnerId = winner.ratingKey
|
|
loserId = loser.ratingKey
|
|
|
|
ratings.getRatings [ winnerId, loserId ], (err, oldRatings) =>
|
|
ratings.updatePlayers winnerId, loserId, ratings.results.WIN, (err, result) =>
|
|
return @message "An error occurred updating rankings :(" if err
|
|
|
|
oldRating = Math.floor(oldRatings[0])
|
|
newRating = Math.floor(result[0])
|
|
@cannedText('RATING_UPDATE', index, oldRating, newRating)
|
|
|
|
oldRating = Math.floor(oldRatings[1])
|
|
newRating = Math.floor(result[1])
|
|
@cannedText('RATING_UPDATE', 1 - index, oldRating, newRating)
|
|
|
|
@emit('ratingsUpdated')
|
|
@sendUpdates()
|
|
|
|
createCondition Conditions.TIMED_BATTLE,
|
|
attach:
|
|
initialize: ->
|
|
@playerTimes = {}
|
|
@lastActionTimes = {}
|
|
now = Date.now()
|
|
|
|
# Set up initial values
|
|
for id in @playerIds
|
|
@playerTimes[id] = now + @TEAM_PREVIEW_TIMER
|
|
|
|
# Set up timers and event listeners
|
|
check = () =>
|
|
@startBattle()
|
|
@sendUpdates()
|
|
@teamPreviewTimerId = setTimeout(check, @TEAM_PREVIEW_TIMER)
|
|
@once('end', => clearTimeout(@teamPreviewTimerId))
|
|
@once('start', => clearTimeout(@teamPreviewTimerId))
|
|
|
|
start: ->
|
|
nowTime = Date.now()
|
|
for id in @playerIds
|
|
@playerTimes[id] = nowTime + @DEFAULT_TIMER
|
|
# Remove first turn since we'll be increasing it again.
|
|
@playerTimes[id] -= @TIMER_PER_TURN_INCREASE
|
|
@startTimer()
|
|
|
|
requestActions: (playerId) ->
|
|
# If a player has selected a move, then there's an amount of time spent
|
|
# between move selection and requesting another action that was "lost".
|
|
# We grant this back here.
|
|
if @lastActionTimes[playerId]
|
|
now = Date.now()
|
|
leftoverTime = now - @lastActionTimes[playerId]
|
|
delete @lastActionTimes[playerId]
|
|
@addTime(playerId, leftoverTime)
|
|
# In either case, we tell people that this player's timer resumes.
|
|
@send('resumeTimer', @id, @getPlayerIndex(playerId))
|
|
|
|
addAction: (playerId, action) ->
|
|
# Record the last action for use
|
|
@lastActionTimes[playerId] = Date.now()
|
|
@recalculateTimers()
|
|
|
|
undoCompletedRequest: (playerId) ->
|
|
delete @lastActionTimes[playerId]
|
|
@recalculateTimers()
|
|
|
|
# Show players updated times
|
|
beginTurn: ->
|
|
remainingTimes = []
|
|
for id in @playerIds
|
|
@addTime(id, @TIMER_PER_TURN_INCREASE)
|
|
remainingTimes.push(@timeRemainingFor(id))
|
|
@send('updateTimers', @id, remainingTimes)
|
|
|
|
continueTurn: ->
|
|
for id in @playerIds
|
|
@send('pauseTimer', @id, @getPlayerIndex(id))
|
|
|
|
spectateBattle: (user) ->
|
|
playerId = user.name
|
|
remainingTimes = (@timeRemainingFor(id) for id in @playerIds)
|
|
user.send('updateTimers', @id, remainingTimes)
|
|
|
|
# Pause timer for players who have already chosen a move.
|
|
if @hasCompletedRequests(playerId)
|
|
index = @getPlayerIndex(playerId)
|
|
timeSinceLastAction = Date.now() - @lastActionTimes[playerId]
|
|
user.send('pauseTimer', @id, index, timeSinceLastAction)
|
|
|
|
extend:
|
|
DEFAULT_TIMER: 3 * 60 * 1000 # three minutes
|
|
TIMER_PER_TURN_INCREASE: 15 * 1000 # fifteen seconds
|
|
TIMER_CAP: 3 * 60 * 1000 # three minutes
|
|
TEAM_PREVIEW_TIMER: 1.5 * 60 * 1000 # 1 minute and 30 seconds
|
|
|
|
startTimer: (msecs) ->
|
|
msecs ?= @DEFAULT_TIMER
|
|
@timerId = setTimeout(@declareWinner.bind(this), msecs)
|
|
@once('end', => clearTimeout(@timerId))
|
|
|
|
addTime: (id, msecs) ->
|
|
@playerTimes[id] += msecs
|
|
remainingTime = @timeRemainingFor(id)
|
|
if remainingTime > @TIMER_CAP
|
|
diff = remainingTime - @TIMER_CAP
|
|
@playerTimes[id] -= diff
|
|
@recalculateTimers()
|
|
@playerTimes[id]
|
|
|
|
recalculateTimers: ->
|
|
playerTimes = for id in @playerIds
|
|
if @lastActionTimes[id] then Infinity else @timeRemainingFor(id)
|
|
leastTime = Math.min(playerTimes...)
|
|
clearTimeout(@timerId)
|
|
if 0 < leastTime < Infinity
|
|
@timerId = setTimeout(@declareWinner.bind(this), leastTime)
|
|
else if leastTime <= 0
|
|
@declareWinner()
|
|
|
|
timeRemainingFor: (playerId) ->
|
|
endTime = @playerTimes[playerId]
|
|
nowTime = @lastActionTimes[playerId] || Date.now()
|
|
return endTime - nowTime
|
|
|
|
playersWithLeastTime: ->
|
|
losingIds = []
|
|
leastTimeRemaining = Infinity
|
|
for id in @playerIds
|
|
timeRemaining = @timeRemainingFor(id)
|
|
if timeRemaining < leastTimeRemaining
|
|
losingIds = [ id ]
|
|
leastTimeRemaining = timeRemaining
|
|
else if timeRemaining == leastTimeRemaining
|
|
losingIds.push(id)
|
|
return losingIds
|
|
|
|
declareWinner: ->
|
|
loserIds = @playersWithLeastTime()
|
|
loserId = @rng.choice(loserIds, "timer")
|
|
index = @getPlayerIndex(loserId)
|
|
winnerIndex = 1 - index
|
|
@timerWin(winnerIndex)
|
|
|
|
timerWin: (winnerIndex) ->
|
|
@tell(Protocol.TIMER_WIN, winnerIndex)
|
|
@emit('end', @playerIds[winnerIndex])
|
|
@sendUpdates()
|
|
|
|
createCondition Conditions.TEAM_PREVIEW,
|
|
attach:
|
|
initialize: ->
|
|
@arranging = true
|
|
@arranged = {}
|
|
|
|
beforeStart: ->
|
|
@tell(Protocol.TEAM_PREVIEW)
|
|
|
|
start: ->
|
|
@arranging = false
|
|
arrangements = @getArrangements()
|
|
@tell(Protocol.REARRANGE_TEAMS, arrangements...)
|
|
for playerId, i in @playerIds
|
|
team = @getTeam(playerId)
|
|
team.arrange(arrangements[i])
|
|
|
|
extendFacade:
|
|
arrangeTeam: (playerId, arrangement) ->
|
|
return false if @battle.hasStarted()
|
|
return false if arrangement not instanceof Array
|
|
team = @battle.getTeam(playerId)
|
|
return false if !team
|
|
return false if arrangement.length != team.size()
|
|
for index, i in arrangement
|
|
return false if isNaN(index)
|
|
return false if !team.pokemon[index]
|
|
return false if arrangement.indexOf(index, i + 1) != -1
|
|
@battle.arrangeTeam(playerId, arrangement)
|
|
@battle.sendUpdates()
|
|
return true
|
|
|
|
extend:
|
|
arrangeTeam: (playerId, arrangement) ->
|
|
return false unless @arranging
|
|
@arranged[playerId] = arrangement
|
|
if _.difference(@playerIds, Object.keys(@arranged)).length == 0
|
|
@startBattle()
|
|
|
|
getArrangements: ->
|
|
for playerId in @playerIds
|
|
@arranged[playerId] || [0...@getTeam(playerId).size()]
|
|
|
|
createCondition Conditions.VISIBLE_TEAM,
|
|
attach:
|
|
initialize: ->
|
|
@tell(Protocol.VISIBLE_TEAM) |