{_} = 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.PRIMAL_LIMIT, validateTeam: (team, genData) -> errors = [] items = team.map((p) -> p.item) primalitems = ["Red Orb", "Blue Orb"] i = 0 for item in items i++ if item in primalitems errors.push("You can only have one Primal Evolution item in your team") if i > 1 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: -> @initTimer() start: -> @startingTimer() 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: -> @onBeginTurn() 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 initTimer: -> @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)) startingTimer: -> 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() onBeginTurn: -> remainingTimes = [] for id in @playerIds @addTime(id, @TIMER_PER_TURN_INCREASE) remainingTimes.push(@timeRemainingFor(id)) @send('updateTimers', @id, remainingTimes) 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)