{Attachment, Status} = require './attachment' {Weather} = require '../../shared/weather' {Protocol} = require '../../shared/protocol' Query = require('./queries') util = require './util' # A single Move in the Pokemon engine. Move objects are constructed in # data/VERSION/moves.coffee, with only one instance per move (for example, # there is only one Flamethrower). These instances are retrieved by the battle # engine. class @Move criticalMultiplier: 2 constructor: (@name, attributes = {}) -> @accuracy = attributes.accuracy || 0 @priority = attributes.priority || 0 @power = attributes.power @target = attributes.target @description = attributes.description || "No description set" @type = attributes.type || '???' @spectra = attributes.damage || '???' @chLevel = attributes.criticalHitLevel || 1 @flags = attributes.flags || [] @flinchChance = (attributes.flinchChance || 0) @ailmentChance = (attributes.ailmentChance || 0) @ailmentId = attributes.ailmentId @primaryBoostStats = attributes.primaryBoostStats @primaryBoostTarget = attributes.primaryBoostTarget @secondaryBoostChance = attributes.secondaryBoostChance || 0 @secondaryBoostStats = attributes.secondaryBoostStats @secondaryBoostTarget = attributes.secondaryBoostTarget @pp = attributes.pp @recoil = attributes.recoil {@minHits, @maxHits} = attributes isPhysical: -> @spectra == 'physical' isSpecial: -> @spectra == 'special' isNonDamaging: -> @spectra == 'non-damaging' hasFlag: (flagName) -> flagName in @flags hasPrimaryEffect: -> @primaryBoostStats? || (@ailmentId != "none" && @ailmentChance == 0) # A secondary effect also includes flinching. hasSecondaryEffect: -> (@ailmentChance > 0 && @ailmentId != "none") || @flinchChance > 0 || @secondaryBoostChance > 0 # Executes this move on several targets. # Only override this method if the move does not need to be # recorded on the enemy pokemon. execute: (battle, user, targets) -> # If there are no targets, then the move should automatically fail. # For example, Helping Hand may not have a target. return @fail(battle, user) if targets.length == 0 # If there were targets, but they are all no longer alive, then the engine # outputs a stock message and quits move execution. targets = targets.filter((p) -> p.isAlive()) if targets.length == 0 battle.cannedText('NO_TARGET') return # The move is executing. Run a hook. @executing(battle, user, targets) targetsHit = [] totalDamage = 0 for target in targets continue if @use(battle, user, target, hitNumber) == false if target.shouldBlockExecution(this, user) == true @afterFail(battle, user, target) continue targetsHit.push(target) if targetsHit.length > 0 targetSlots = targetsHit.map (target) -> return [ battle.playerIds.indexOf(target.playerId), target.team.indexOf(target) ] user.tell(Protocol.MOVE_SUCCESS, targetSlots, @name) for target in targetsHit numHits = @calculateNumberOfHits(battle, user, targets) wasSlept = user.has(Status.Sleep) for hitNumber in [1..numHits] isDirect = @isDirectHit(battle, user, target) damage = @hit(battle, user, target, hitNumber, isDirect) || 0 @afterHit(battle, user, target, damage, isDirect) totalDamage += damage break if target.isFainted() || user.isFainted() || (!wasSlept && user.has(Status.Sleep)) if numHits > 1 battle.message @numHitsMessage Math.min(hitNumber, numHits) # Target faints if it has 0 HP. for target in targets when target.isFainted() target.faint() # Recoil moves if totalDamage > 0 && @recoil < 0 && !user.hasAbility("Rock Head") recoil = Math.round(totalDamage * -@recoil / 100) if user.damage(recoil) battle.cannedText('RECOIL', user) # If the move hit 1+ times, query the user's afterAllHits event. # If the user is affected by Sheer Force, these are all ignored. if targetsHit.length > 0 && (!user.hasAbility("Sheer Force") || !@hasSecondaryEffect()) user.afterAllHits(this) @afterAllHits(battle, user) for target in targetsHit target.afterAllHitsTarget(this, user) # A hook with a default implementation of returning false on a type immunity. # If `use` returns false, the `hit` hook is never called. use: (battle, user, target, hitNumber) -> if target.isImmune(@getType(battle, user, target), user: user, move: this) battle.cannedText('IMMUNITY', target) @afterFail(battle, user, target) return false if @willMiss(battle, user, target) battle.cannedText('MOVE_MISS', target) @afterMiss(battle, user, target) return false if battle.hasWeather(Weather.HARSHSUN) and @getType(battle, user, target) == "Water" battle.cannedText('HARSHSUN_MOVEFAIL') return false if battle.hasWeather(Weather.HEAVYRAIN) and @getType(battle, user, target) == "Fire" battle.cannedText('HEAVYRAIN_MOVEFAIL') return false # Calculates damage, deals damage, and returns the amount of damage dealt hit: (battle, user, target, hitNumber, isDirect) -> damage = @calculateDamage(battle, user, target, hitNumber, isDirect) if damage > 0 previousHP = target.get(Attachment.Substitute)?.hp ? target.currentHP damage = target.damage(damage, direct: isDirect, source: "move") if damage != 0 percent = Math.floor(100 * damage / target.stat('hp')) battle.cannedText('GOT_HIT', target, percent) else currentHP = target.get(Attachment.Substitute)?.hp ? target.currentHP damage = previousHP - Math.max(0, currentHP) return damage # `hit` may be overridden, but we still want to run these callbacks. afterHit: (battle, user, target, damage, isDirect) -> # Drain moves if damage > 0 && @recoil > 0 amount = Math.round(damage * @recoil / 100) user.drain(amount, target) battle.cannedText('DRAIN', target) battle.cannedText('ABSORB', user) if @shouldTriggerSecondary(battle, user, target, damage, isDirect) @triggerSecondaryEffect(battle, user, target) user.afterSuccessfulHit(this, user, target, damage, isDirect) target.afterBeingHit(this, user, target, damage, isDirect) @afterSuccessfulHit(battle, user, target, damage, isDirect) user.update() target.update() # Miscellaneous target.recordHit(user, damage, this, battle.turn, isDirect) # A hook that runs when the move is finally executing. executing: (battle, user, targets) -> # A hook that executes after a pokemon has been successfully damaged by # a standard move. If execute is overriden, this will not execute. afterSuccessfulHit: (battle, user, target, damage) -> # A hook that executes after a pokemon misses an attack. If execute is # overriden, this will not execute. afterMiss: (battle, user, target) -> # A hook that executes after a pokemon fails while using a move (NOT a miss.) # Examples: Target is immune, target successfully uses Protect/Detect afterFail: (battle, user, target) -> # A hook that executes after all hits have completed. afterAllHits: (battle, user) -> # A hook that executes when asking if this move should do direct damage isDirectHit: (battle, user, target) -> return true if @hasFlag('authentic') return !target.has(Attachment.Substitute) # A hook that executes once a move fails. fail: (battle, user) -> battle.cannedText('MOVE_FAIL') numHitsMessage: (hitNumber) -> return "Hit #{hitNumber} time(s)!" # A hook that is only used by special "specific-move" targets. getTargets: (battle, user) -> throw new Error("Move #{@name} has not implemented getTargets.") calculateDamage: (battle, user, target, hitNumber=1, isDirect) -> return 0 if @basePower(battle, user, target, hitNumber) == 0 user.crit = @isCriticalHit(battle, user, target) damage = @baseDamage(battle, user, target, hitNumber) # TODO: Multi-target modifier. damage = @modify(damage, @weatherModifier(battle, user, target)) damage = damage * @criticalMultiplier if user.crit damage = Math.floor(((100 - battle.rng.randInt(0, 15, "damage roll")) * damage) / 100) damage = @modify(damage, @stabModifier(battle, user, target)) effectiveness = @typeEffectiveness(battle, user, target) damage = Math.floor(effectiveness * damage) damage = Math.floor(@burnCalculation(user) * damage) damage = Math.max(damage, 1) damage = @modify(damage, @modifyDamage(battle, user, target, hitNumber)) if effectiveness < 1 battle.cannedText('NOT_VERY_EFFECTIVE') else if effectiveness > 1 battle.cannedText('SUPER_EFFECTIVE') if user.crit battle.cannedText('CRITICAL_HIT') target.informCriticalHit() damage canMiss: (battle, user, target) -> return true # An accuracy of 0 means this move always hits. A negative accuracy means the # target is invulnerable, even for 0-accuracy moves. A move always hits, regardless # of accuracy and invulnerability, under any of the following conditions: # * The user or target has No Guard # * The user is locked onto the target (Lock-On, Mind Reader) # * The move has a "never-miss" effect built in # (Helping Hand, XY Toxic used by Poison types) willMiss: (battle, user, target) -> return false if user.hasAbility("No Guard") || target.hasAbility("No Guard") return false if user.get(Attachment.LockOn)?.target == target return false if !@canMiss(battle, user, target) accuracy = @chanceToHit(battle, user, target) accuracy = 100 if accuracy == 0 console.log(accuracy) battle.rng.randInt(1, 100, "miss") > accuracy chanceToHit: (battle, user, target) -> return 0 if user == target userBoosts = user.editBoosts(ignoreAccuracy: target.hasAbility("Unaware")) targetBoosts = target.editBoosts(ignoreEvasion: user.hasAbility("Unaware")) accuracy = @getAccuracy(battle, user, target) # TODO: Find out how accuracy/evasion is chained and do the rest properly accuracy = 50 if @isNonDamaging() && accuracy > 50 && target.hasAbility("Wonder Skin") if userBoosts.accuracy > 0 accuracy = Math.floor(accuracy * (3 + userBoosts.accuracy) / 3) else if userBoosts.accuracy < 0 accuracy = Math.floor(accuracy / (3 - userBoosts.accuracy) * 3) if targetBoosts.evasion > 0 accuracy = Math.floor(accuracy / (3 + targetBoosts.evasion) * 3) else if targetBoosts.evasion < 0 accuracy = Math.floor(accuracy * (3 - targetBoosts.evasion) / 3) accuracy = user.editAccuracy(accuracy, this, target) accuracy = target.editEvasion(accuracy, this, user) accuracy getAccuracy: (battle, user, target) -> @accuracy weatherModifier: (battle, user, target) -> # TODO: This is wrong. type = @getType(battle, user, target) if type == 'Fire' && battle.hasWeather(Weather.SUN) 0x1800 else if type == 'Fire' && battle.hasWeather(Weather.RAIN) 0x800 else if type == 'Water' && battle.hasWeather(Weather.RAIN) 0x1800 else if type == 'Water' && battle.hasWeather(Weather.SUN) 0x800 else if type == 'Dark' && battle.hasWeather(Weather.MOON) 0x159A else if type == 'Fairy' && battle.hasWeather(Weather.MOON) 0xC00 else 0x1000 stabModifier: (battle, user, target) -> type = @getType(battle, user, target) if user.hasType(type) return 0x2000 if user.hasAbility("Adaptability") return 0x1800 return 0x1000 ignoresImmunities: -> @isNonDamaging() typeEffectiveness: (battle, user, target) -> type = @getType(battle, user, target) effect = target.effectivenessOf(type, user: user, move: this) if battle.hasWeather(Weather.DELTASTREAM) and target.hasType("Flying") neweffect = 1 for targettype in target.types typeeffect = util.typeEffectiveness(type, targettype) if targettype == "Flying" and typeeffect > 1 typeeffect = 1 neweffect * typeeffect effect = neweffect if target.hasAbility("Ethereal Shroud") ghosteffect = util.typeEffectiveness(type, ["Ghost"]) if ghosteffect < 1 effect *= ghosteffect effect burnCalculation: (user) -> if @isPhysical() && !user.hasAbility("Guts") && user.has(Status.Burn) .5 else 1 basePower: (battle, user, target, hitNumber) -> @power shouldTriggerSecondary: (battle, user, target, damage, isDirect) -> return false if !@hasSecondaryEffect() return false if !isDirect && @secondaryBoostTarget != 'self' return false if user.hasAbility("Sheer Force") return false if target.hasAbility("Shield Dust") && @secondaryBoostTarget != 'self' return true triggerSecondaryEffect: (battle, user, target) -> # Multiply chances by 2 if the user has Serene Grace. chanceMultiplier = (if user.hasAbility("Serene Grace") then 2 else 1) # Secondary effects if @ailmentChance > 0 && battle.rng.randInt(0, 99, "secondary effect") < @ailmentChance * chanceMultiplier target.attach(battle.getAilmentEffect(this), source: user) # Secondary boosts if @secondaryBoostChance > 0 && battle.rng.randInt(0, 99, "secondary boost") < @secondaryBoostChance * chanceMultiplier pokemon = (if @secondaryBoostTarget == 'self' then user else target) pokemon.boost(@secondaryBoostStats, user) # Flinching. In the game, flinching is treated subtly different than # secondary effects. One result is that the Fang moves can both inflict # a secondary effect as well as flinch. if @flinchChance > 0 && battle.rng.randInt(0, 99, "flinch") < @flinchChance * chanceMultiplier target.attach(Attachment.Flinch) isCriticalHit: (battle, attacker, defender) -> return false if defender.team?.has(Attachment.LuckyChant) return false if defender.ability?.preventsCriticalHits chLevel = @criticalHitLevel(battle, attacker, defender) rand = battle.rng.next("ch") @determineCriticalHitFromLevel(chLevel, rand) determineCriticalHitFromLevel: (level, rand) -> switch level when -1 true when 1 rand < 0.0625 when 2 rand < 0.125 when 3 rand < 0.25 when 4 rand < 1/3 else rand < .5 criticalHitLevel: (battle, attacker, defender) -> # -1 means always crits return @chLevel if @chLevel == -1 stage = @chLevel stage += 1 if attacker.hasAbility('Super Luck') stage += 2 if attacker.has(Attachment.FocusEnergy) stage += attacker.criticalModifier() stage modify: (number, modifier) -> Math.ceil((number * modifier) / 0x1000 - 0.5) baseDamage: (battle, user, target, hitNumber=1) -> floor = Math.floor uStat = @pickAttackStat(user, target) tStat = @pickDefenseStat(user, target) if battle.hasWeather(Weather.SAND) && target.hasType("Rock") && @isSpecial() tStat = @modify(tStat, 0x1800) damage = floor((2 * user.level) / 5 + 2) damage *= @basePower(battle, user, target, hitNumber) damage = @modify(damage, @modifyBasePower(battle, user, target)) damage *= @modify(uStat, @modifyAttack(battle, user, target)) damage = floor(damage / tStat) damage = floor(damage / 50) damage += 2 damage calculateNumberOfHits: (battle, user, targets) -> numHits = user.calculateNumberOfHits(this, targets) if numHits numHits else if @minHits == @maxHits @maxHits else if @minHits == 2 && @maxHits == 5 # hard coding moves like fury swipes to have 2-3 hits have a 1/3 chance, and 4-5 have 1/6th battle.rng.choice([2, 2, 3, 3, 4, 5], "num hits") else battle.rng.randInt(@minHits, @maxHits, "num hits") modifyBasePower: (battle, user, target) -> modify = Query.modifiers("modifyBasePower", user.attachments.all(), this, target) modify = @modify(modify, Query.modifiers("modifyBasePowerTarget", target.attachments.all(), this, user)) modifyDamage: (battle, user, target, hitNumber) -> modify = Query.modifiers('modifyDamageTarget', target.team.attachments.all(), this, user, hitNumber) modify = @modify(modify, Query.modifiers('modifyDamage', user.attachments.all(), this, target, hitNumber)) modify = @modify(modify, Query.modifiers('modifyDamageTarget', target.attachments.all(), this, user, hitNumber)) modifyAttack: (battle, user, target) -> modify = Query.modifiers('modifyAttack', user.attachments.all(), this, target) modify = @modify(modify, Query.modifiers('modifyAttackTarget', target.attachments.all(), this, user)) getType: (battle, user, target) -> type = user.editMoveType(@type, target) type pickAttackStat: (user, target) -> stat = (if @isPhysical() then 'attack' else 'specialAttack') stat = 'specialAttack' if user.hasAbility("Spectral Jaws") and @hasFlag("bite") user.stat(stat, ignoreNegativeBoosts: user.crit, ignoreOffense: target.hasAbility("Unaware")) pickDefenseStat: (user, target) -> stat = (if @isPhysical() then 'defense' else 'specialDefense') stat = 'specialDefense' if user.hasAbility("Spectral Jaws") and @hasFlag("bite") target.stat(stat, ignorePositiveBoosts: user.crit, ignoreDefense: user.hasAbility("Unaware")) toString: -> "[Move name:#{@name}]"