#=============================================================================== # #=============================================================================== class Battle::Scene ANIMATION_DEFAULTS = [:TACKLE, :DEFENSECURL] # With target, without target ANIMATION_DEFAULTS_FOR_TYPE_CATEGORY = { :NORMAL => [:TACKLE, :SONICBOOM, :DEFENSECURL, :BODYSLAM, nil, :TAILWHIP], :FIGHTING => [:MACHPUNCH, :AURASPHERE, :BULKUP, nil, nil, nil], :FLYING => [:WINGATTACK, :GUST, :ROOST, nil, :AIRCUTTER, :FEATHERDANCE], :POISON => [:POISONSTING, :SLUDGE, :ACIDARMOR, nil, :ACID, :POISONPOWDER], :GROUND => [:SANDTOMB, :MUDSLAP, :MUDSPORT, :EARTHQUAKE, :EARTHPOWER, :SANDATTACK], :ROCK => [:ROCKTHROW, :POWERGEM, :ROCKPOLISH, :ROCKSLIDE, nil, :SANDSTORM], :BUG => [:TWINEEDLE, :BUGBUZZ, :QUIVERDANCE, nil, :STRUGGLEBUG, :STRINGSHOT], :GHOST => [:ASTONISH, :SHADOWBALL, :GRUDGE, nil, nil, :CONFUSERAY], :STEEL => [:IRONHEAD, :MIRRORSHOT, :IRONDEFENSE, nil, nil, :METALSOUND], :FIRE => [:FIREPUNCH, :EMBER, :SUNNYDAY, nil, :INCINERATE, :WILLOWISP], :WATER => [:CRABHAMMER, :WATERGUN, :AQUARING, nil, :SURF, :WATERSPORT], :GRASS => [:VINEWHIP, :RAZORLEAF, :COTTONGUARD, nil, nil, :SPORE], :ELECTRIC => [:THUNDERPUNCH, :THUNDERSHOCK, :CHARGE, nil, :DISCHARGE, :THUNDERWAVE], :PSYCHIC => [:ZENHEADBUTT, :CONFUSION, :CALMMIND, nil, :SYNCHRONOISE, :MIRACLEEYE], :ICE => [:ICEPUNCH, :ICEBEAM, :MIST, :AVALANCHE, :POWDERSNOW, :HAIL], :DRAGON => [:DRAGONCLAW, :DRAGONRAGE, :DRAGONDANCE, nil, :TWISTER, nil], :DARK => [:KNOCKOFF, :DARKPULSE, :HONECLAWS, nil, :SNARL, :EMBARGO], :FAIRY => [:TACKLE, :FAIRYWIND, :MOONLIGHT, nil, :DAZZLINGGLEAM, :SWEETKISS] } # Animates the battle intro. def pbBattleIntroAnimation # Make everything appear introAnim = Animation::Intro.new(@sprites, @viewport, @battle) loop do introAnim.update pbUpdate break if introAnim.animDone? end introAnim.dispose # Post-appearance activities # Trainer battle: get ready to show the party lineups (they are brought # on-screen by a separate animation) if @battle.trainerBattle? # NOTE: Here is where you'd make trainer sprites animate if they had an # entrance animation. Be sure to set it up like a Pokémon entrance # animation, i.e. add them to @animations so that they can play out # while party lineups appear and messages show. pbShowPartyLineup(0, true) pbShowPartyLineup(1, true) return end # Wild battle: play wild Pokémon's intro animations (including cry), show # data box(es), return the wild Pokémon's sprite(s) to normal colour, show # shiny animation(s) # Set up data box animation @battle.sideSizes[1].times do |i| idxBattler = (2 * i) + 1 next if !@battle.battlers[idxBattler] dataBoxAnim = Animation::DataBoxAppear.new(@sprites, @viewport, idxBattler) @animations.push(dataBoxAnim) end # Set up wild Pokémon returning to normal colour and playing intro # animations (including cry) @animations.push(Animation::Intro2.new(@sprites, @viewport, @battle.sideSizes[1])) # Play all the animations while inPartyAnimation? pbUpdate end # Show shiny animation for wild Pokémon if @battle.showAnims @battle.sideSizes[1].times do |i| idxBattler = (2 * i) + 1 next if !@battle.battlers[idxBattler] || !@battle.battlers[idxBattler].shiny? if Settings::SUPER_SHINY && @battle.battlers[idxBattler].super_shiny? pbCommonAnimation("SuperShiny", @battle.battlers[idxBattler]) else pbCommonAnimation("Shiny", @battle.battlers[idxBattler]) end end end end # Animates a party lineup appearing for the given side. def pbShowPartyLineup(side, fullAnim = false) @animations.push( Animation::LineupAppear.new(@sprites, @viewport, side, @battle.pbParty(side), @battle.pbPartyStarts(side), fullAnim) ) return if fullAnim while inPartyAnimation? pbUpdate end end # Animates an opposing trainer sliding in from off-screen. Will animate a # previous trainer that is already on-screen slide off first. Used at the end # of battle. def pbShowOpponent(idxTrainer) # Set up trainer appearing animation appearAnim = Animation::TrainerAppear.new(@sprites, @viewport, idxTrainer) @animations.push(appearAnim) # Play the animation while inPartyAnimation? pbUpdate end end # Animates a trainer's sprite and party lineup hiding (if they are visible). # Animates a Pokémon being sent out into battle, then plays the shiny # animation for it if relevant. # sendOuts is an array; each element is itself an array: [idxBattler,pkmn] def pbSendOutBattlers(sendOuts, startBattle = false) return if sendOuts.length == 0 # If party balls are still appearing, wait for them to finish showing up, as # the FadeAnimation will make them disappear. while inPartyAnimation? pbUpdate end @briefMessage = false # Make all trainers and party lineups disappear (player-side trainers may # animate throwing a Poké Ball) if @battle.opposes?(sendOuts[0][0]) fadeAnim = Animation::TrainerFade.new(@sprites, @viewport, startBattle) else fadeAnim = Animation::PlayerFade.new(@sprites, @viewport, startBattle) end # For each battler being sent out, set the battler's sprite and create two # animations (the Poké Ball moving and battler appearing from it, and its # data box appearing) sendOutAnims = [] sendOuts.each_with_index do |b, i| pkmn = @battle.battlers[b[0]].effects[PBEffects::Illusion] || b[1] pbChangePokemon(b[0], pkmn) pbRefresh if @battle.opposes?(b[0]) sendOutAnim = Animation::PokeballTrainerSendOut.new( @sprites, @viewport, @battle.pbGetOwnerIndexFromBattlerIndex(b[0]) + 1, @battle.battlers[b[0]], startBattle, i ) else sendOutAnim = Animation::PokeballPlayerSendOut.new( @sprites, @viewport, @battle.pbGetOwnerIndexFromBattlerIndex(b[0]) + 1, @battle.battlers[b[0]], startBattle, i ) end dataBoxAnim = Animation::DataBoxAppear.new(@sprites, @viewport, b[0]) sendOutAnims.push([sendOutAnim, dataBoxAnim, false]) end # Play all animations loop do fadeAnim.update sendOutAnims.each do |a| next if a[2] a[0].update a[1].update if a[0].animDone? a[2] = true if a[1].animDone? end pbUpdate break if !inPartyAnimation? && sendOutAnims.none? { |a| !a[2] } end fadeAnim.dispose sendOutAnims.each do |a| a[0].dispose a[1].dispose end # Play shininess animations for shiny Pokémon sendOuts.each do |b| next if !@battle.showAnims || !@battle.battlers[b[0]].shiny? if Settings::SUPER_SHINY && @battle.battlers[b[0]].super_shiny? pbCommonAnimation("SuperShiny", @battle.battlers[b[0]]) else pbCommonAnimation("Shiny", @battle.battlers[b[0]]) end end end # Animates a Pokémon being recalled into its Poké Ball and its data box hiding. def pbRecall(idxBattler) @briefMessage = false # Recall animation recallAnim = Animation::BattlerRecall.new(@sprites, @viewport, @battle.battlers[idxBattler]) loop do recallAnim&.update pbUpdate break if recallAnim.animDone? end recallAnim.dispose # Data box disappear animation dataBoxAnim = Animation::DataBoxDisappear.new(@sprites, @viewport, idxBattler) loop do dataBoxAnim.update pbUpdate break if dataBoxAnim.animDone? end dataBoxAnim.dispose end #----------------------------------------------------------------------------- # Ability splash bar animations. #----------------------------------------------------------------------------- def pbShowAbilitySplash(battler) return if !USE_ABILITY_SPLASH side = battler.index % 2 pbHideAbilitySplash(battler) if @sprites["abilityBar_#{side}"].visible @sprites["abilityBar_#{side}"].battler = battler abilitySplashAnim = Animation::AbilitySplashAppear.new(@sprites, @viewport, side) loop do abilitySplashAnim.update pbUpdate break if abilitySplashAnim.animDone? end abilitySplashAnim.dispose end def pbHideAbilitySplash(battler) return if !USE_ABILITY_SPLASH side = battler.index % 2 return if !@sprites["abilityBar_#{side}"].visible abilitySplashAnim = Animation::AbilitySplashDisappear.new(@sprites, @viewport, side) loop do abilitySplashAnim.update pbUpdate break if abilitySplashAnim.animDone? end abilitySplashAnim.dispose end def pbReplaceAbilitySplash(battler) return if !USE_ABILITY_SPLASH pbShowAbilitySplash(battler) end #----------------------------------------------------------------------------- # HP change animations. #----------------------------------------------------------------------------- # Shows a HP-changing common animation and animates a data box's HP bar. # Called by def pbReduceHP, def pbRecoverHP. def pbHPChanged(battler, oldHP, showAnim = false) @briefMessage = false if battler.hp > oldHP pbCommonAnimation("HealthUp", battler) if showAnim && @battle.showAnims elsif battler.hp < oldHP pbCommonAnimation("HealthDown", battler) if showAnim && @battle.showAnims end @sprites["dataBox_#{battler.index}"].animate_hp(oldHP, battler.hp) while @sprites["dataBox_#{battler.index}"].animating_hp? pbUpdate end end def pbDamageAnimation(battler, effectiveness = 0) @briefMessage = false # Damage animation damageAnim = Animation::BattlerDamage.new(@sprites, @viewport, battler.index, effectiveness) loop do damageAnim.update pbUpdate break if damageAnim.animDone? end damageAnim.dispose end # Animates battlers flashing and data boxes' HP bars because of damage taken # by an attack. targets is an array, which are all animated simultaneously. # Each element in targets is also an array: [battler, old HP, effectiveness] def pbHitAndHPLossAnimation(targets) @briefMessage = false # Set up animations damageAnims = [] targets.each do |t| anim = Animation::BattlerDamage.new(@sprites, @viewport, t[0].index, t[2]) damageAnims.push(anim) @sprites["dataBox_#{t[0].index}"].animate_hp(t[1], t[0].hp) end # Update loop loop do damageAnims.each { |a| a.update } pbUpdate allDone = true targets.each do |t| next if !@sprites["dataBox_#{t[0].index}"].animating_hp? allDone = false break end next if !allDone damageAnims.each do |a| next if a.animDone? allDone = false break end next if !allDone break end damageAnims.each { |a| a.dispose } end #----------------------------------------------------------------------------- # Animates a data box's Exp bar. def pbEXPBar(battler, startExp, endExp, tempExp1, tempExp2) return if !battler || endExp == startExp startExpLevel = tempExp1 - startExp endExpLevel = tempExp2 - startExp expRange = endExp - startExp dataBox = @sprites["dataBox_#{battler.index}"] dataBox.animate_exp(startExpLevel, endExpLevel, expRange) while dataBox.animating_exp? pbUpdate end end # Shows stats windows upon a Pokémon levelling up. def pbLevelUp(pkmn, _battler, oldTotalHP, oldAttack, oldDefense, oldSpAtk, oldSpDef, oldSpeed) pbTopRightWindow( _INTL("Max. HP+{1}\nAttack+{2}\nDefense+{3}\nSp. Atk+{4}\nSp. Def+{5}\nSpeed+{6}", pkmn.totalhp - oldTotalHP, pkmn.attack - oldAttack, pkmn.defense - oldDefense, pkmn.spatk - oldSpAtk, pkmn.spdef - oldSpDef, pkmn.speed - oldSpeed) ) pbTopRightWindow( _INTL("Max. HP{1}\nAttack{2}\nDefense{3}\nSp. Atk{4}\nSp. Def{5}\nSpeed{6}", pkmn.totalhp, pkmn.attack, pkmn.defense, pkmn.spatk, pkmn.spdef, pkmn.speed) ) end # Animates a Pokémon fainting. def pbFaintBattler(battler) @briefMessage = false old_height = @sprites["pokemon_#{battler.index}"].src_rect.height # Pokémon plays cry and drops down, data box disappears faintAnim = Animation::BattlerFaint.new(@sprites, @viewport, battler.index, @battle) dataBoxAnim = Animation::DataBoxDisappear.new(@sprites, @viewport, battler.index) loop do faintAnim.update dataBoxAnim.update pbUpdate break if faintAnim.animDone? && dataBoxAnim.animDone? end faintAnim.dispose dataBoxAnim.dispose @sprites["pokemon_#{battler.index}"].src_rect.height = old_height end #----------------------------------------------------------------------------- # Animates throwing a Poké Ball at a Pokémon in an attempt to catch it. #----------------------------------------------------------------------------- def pbThrow(ball, shakes, critical, targetBattler, showPlayer = false) @briefMessage = false captureAnim = Animation::PokeballThrowCapture.new( @sprites, @viewport, ball, shakes, critical, @battle.battlers[targetBattler], showPlayer ) loop do captureAnim.update pbUpdate break if captureAnim.animDone? && !inPartyAnimation? end captureAnim.dispose end def pbThrowSuccess return if @battle.opponent @briefMessage = false pbMEPlay(pbGetWildCaptureME) timer_start = System.uptime loop do pbUpdate break if System.uptime - timer_start >= 3.5 end pbMEStop end def pbHideCaptureBall(idxBattler) # NOTE: It's not really worth writing a whole Battle::Scene::Animation class # for making the capture ball fade out. ball = @sprites["captureBall"] return if !ball # Data box disappear animation dataBoxAnim = Animation::DataBoxDisappear.new(@sprites, @viewport, idxBattler) timer_start = System.uptime loop do dataBoxAnim.update ball.opacity = lerp(255, 0, 1.0, timer_start, System.uptime) pbUpdate break if dataBoxAnim.animDone? && ball.opacity <= 0 end dataBoxAnim.dispose end def pbThrowAndDeflect(ball, idxBattler) @briefMessage = false throwAnim = Animation::PokeballThrowDeflect.new( @sprites, @viewport, ball, @battle.battlers[idxBattler] ) loop do throwAnim.update pbUpdate break if throwAnim.animDone? end throwAnim.dispose end #----------------------------------------------------------------------------- # Hides all battler shadows before yielding to a move animation, and then # restores the shadows afterwards. def pbSaveShadows # Remember which shadows were visible shadows = Array.new(@battle.battlers.length) do |i| shadow = @sprites["shadow_#{i}"] ret = (shadow) ? shadow.visible : false shadow.visible = false if shadow next ret end # Yield to other code, i.e. playing an animation yield # Restore shadow visibility @battle.battlers.length.times do |i| shadow = @sprites["shadow_#{i}"] shadow.visible = shadows[i] if shadow end end #----------------------------------------------------------------------------- # Loads a move animation. #----------------------------------------------------------------------------- # Returns an array of GameData::Animation if a new animation(s) is found. # Return [animation index, shouldn't be flipped] if an old animation is found. def find_move_animation(move_id, version, user_index) # Get animation anims = find_move_animation_for_move(move_id, version, user_index) return anims if anims # Get information to decide which default animation to try move_data = GameData::Move.get(move_id) target_data = GameData::Target.get(move_data.target) move_type = move_data.type default_idx = move_data.category default_idx += 3 if target_data.num_targets > 1 || (target_data.num_targets > 0 && move_data.status?) # Check for a default animation wanted_move = ANIMATION_DEFAULTS_FOR_TYPE_CATEGORY[move_type][default_idx] anims = find_move_animation_for_move(wanted_move, 0, user_index) return anims if anims if default_idx >= 3 wanted_move = ANIMATION_DEFAULTS_FOR_TYPE_CATEGORY[move_type][default_idx - 3] anims = find_move_animation_for_move(wanted_move, 0, user_index) return anims if anims return nil if ANIMATION_DEFAULTS.include?(wanted_move) # No need to check for these animations twice end # Use Tackle or Defense Curl's animation if target_data.num_targets == 0 && target.data.id != :None return find_move_animation_for_move(ANIMATION_DEFAULTS[1], 0, user_index) end return find_move_animation_for_move(ANIMATION_DEFAULTS[0], 0, user_index) end # Find an animation(s) for the given move_id. def find_move_animation_for_move(move_id, version, user_index) # Find new animation anims = try_get_better_move_animation(move_id, version, user_index) return anims if anims if version > 0 anims = try_get_better_move_animation(move_id, 0, user_index) return anims if anims end # Find old animation anim = pbFindMoveAnimDetails(move_id, user_index, version) return anim end # Finds a new animation for the given move_id and version. Prefers opposing # animations if the user is opposing. Can return multiple animations. def try_get_better_move_animation(move_id, version, user_index) ret = [] backup_ret = [] GameData::Animation.each do |anim| next if !anim.move_animation? || anim.ignore next if anim.move != move_id.to_s next if anim.version != version if !user_index ret.push(anim) next end if user_index.even? # User is on player's side ret.push(anim) if !anim.opposing_animation? else # User is on opposing side (anim.opposing_animation?) ? ret.push(anim) : backup_ret.push(anim) end end return ret if !ret.empty? return backup_ret if !backup_ret.empty? return nil end # Returns the animation ID to use for a given move/user. Returns nil if that # move has no animations defined for it. def pbFindMoveAnimDetails(moveID, idxUser, hitNum = 0) real_move_id = GameData::Move.try_get(moveID)&.id || moveID anims = pbLoadBattleAnimations return nil if !anims anim_id = -1 foe_anim_id = -1 no_flip = false anims.length.times do |i| next if !anims[i] if anims[i].name[/^OppMove\:\s*(.*)$/] if GameData::Move.exists?($~[1]) moveid = GameData::Move.get($~[1]).id foe_anim_id = i if moveid == real_move_id end elsif anims[i].name[/^Move\:\s*(.*)$/] if GameData::Move.exists?($~[1]) moveid = GameData::Move.get($~[1]).id anim_id = i if moveid == real_move_id end end end if (idxUser & 1) == 0 # On player's side anim = anim_id else # On opposing side anim = foe_anim_id no_flip = true if anim >= 0 anim = anim_id if anim < 0 end return [anim + hitNum, no_flip] if anim >= 0 return nil end #----------------------------------------------------------------------------- # Loads a common animation. #----------------------------------------------------------------------------- def try_get_better_common_animation(anim_name, user_index) # Find a new format common animation to play ret = [] backup_ret = [] GameData::Animation.each do |anim| next if !anim.common_animation? || anim.ignore next if anim.move != anim_name if !user_index ret.push(anim) next end if user_index.even? # User is on player's side ret.push(anim) if !anim.opposing_animation? else # User is on opposing side (anim.opposing_animation?) ? ret.push(anim) : backup_ret.push(anim) end end return ret if !ret.empty? return backup_ret if !backup_ret.empty? # Find an old format common animation to play target = target[0] if target.is_a?(Array) animations = pbLoadBattleAnimations return nil if !animations animations.each do |anim| next if !anim || anim.name != "Common:" + anim_name ret = anim break end return ret end #----------------------------------------------------------------------------- # Plays a move/common animation. #----------------------------------------------------------------------------- # Plays a move animation. def pbAnimation(move_id, user, targets, version = 0) anims = find_move_animation(move_id, version, user&.index) return if !anims || anims.empty? if anims[0].is_a?(GameData::Animation) # New format animation pbSaveShadows do # NOTE: anims.sample is a random valid animation. play_better_animation(anims.sample, user, targets) end else # Old format animation anim = anims[0] target = (targets.is_a?(Array)) ? targets[0] : targets animations = pbLoadBattleAnimations return if !animations pbSaveShadows do if anims[1] # On opposing side and using OppMove animation pbAnimationCore(animations[anim], target, user, true) else # On player's side, and/or using Move animation pbAnimationCore(animations[anim], user, target) end end end end # Plays a common animation. def pbCommonAnimation(anim_name, user = nil, target = nil) return if nil_or_empty?(anim_name) # Find an animation to play (new format or old format) anims = try_get_better_common_animation(anim_name, user.index) return if !anims # Play a new format animation if anims.is_a?(Array) # NOTE: anims.sample is a random valid animation. play_better_animation(anims.sample, user, target) return end # Play an old format animation target = target[0] if target.is_a?(Array) pbAnimationCore(anims, user, target || user) end # Ball burst common animations should have a focus of "Target" and a priority # of "Front". # TODO: This is unused. It also doesn't support the new animation format. def pbBallBurstCommonAnimation(_picture_ex, anim_name, battler, target_x, target_y) return if nil_or_empty?(anim_name) animations = pbLoadBattleAnimations anim = animations&.get_from_name("Common:" + anim_name) return if !anim animPlayer = PBAnimationPlayerX.new(anim, battler, nil, self) animPlayer.discard_user_and_target_sprites # Don't involve user/target in animation animPlayer.set_target_origin(target_x, target_y) animPlayer.start @animations.push(animPlayer) end #----------------------------------------------------------------------------- def play_better_animation(anim_data, user, targets) return if !anim_data @briefMessage = false # Memorize old battler coordinates, to be reset after the animation old_battler_coords = [] if user sprite = @sprites["pokemon_#{user.index}"] old_battler_coords[user.index] = [sprite.x, sprite.y] end if targets targets.each do |target| sprite = @sprites["pokemon_#{target.index}"] old_battler_coords[target.index] = [sprite.x, sprite.y] end end # Create animation player anim_player = AnimationPlayer.new(anim_data, user, targets, self) anim_player.set_up # Play animation anim_player.start loop do pbUpdate anim_player.update break if anim_player.can_continue_battle? end anim_player.dispose # Restore old battler coordinates old_battler_coords.each_with_index do |values, i| next if !values sprite = @sprites["pokemon_#{i}"] sprite.x = values[0] sprite.y = values[1] end end def pbAnimationCore(animation, user, target, oppMove = false) return if !animation @briefMessage = false userSprite = (user) ? @sprites["pokemon_#{user.index}"] : nil targetSprite = (target) ? @sprites["pokemon_#{target.index}"] : nil # Remember the original positions of Pokémon sprites oldUserX = (userSprite) ? userSprite.x : 0 oldUserY = (userSprite) ? userSprite.y : 0 oldTargetX = (targetSprite) ? targetSprite.x : oldUserX oldTargetY = (targetSprite) ? targetSprite.y : oldUserY # Create the animation player animPlayer = PBAnimationPlayerX.new(animation, user, target, self, oppMove) # Apply a transformation to the animation based on where the user and target # actually are. Get the centres of each sprite. userHeight = (userSprite&.bitmap && !userSprite.bitmap.disposed?) ? userSprite.bitmap.height : 128 if targetSprite targetHeight = (targetSprite.bitmap && !targetSprite.bitmap.disposed?) ? targetSprite.bitmap.height : 128 else targetHeight = userHeight end animPlayer.setLineTransform( FOCUSUSER_X, FOCUSUSER_Y, FOCUSTARGET_X, FOCUSTARGET_Y, oldUserX, oldUserY - (userHeight / 2), oldTargetX, oldTargetY - (targetHeight / 2) ) # Play the animation animPlayer.start loop do animPlayer.update pbUpdate break if animPlayer.animDone? end animPlayer.dispose # Return Pokémon sprites to their original positions if userSprite userSprite.x = oldUserX userSprite.y = oldUserY userSprite.pbSetOrigin end if targetSprite targetSprite.x = oldTargetX targetSprite.y = oldTargetY targetSprite.pbSetOrigin end end end