From a393ba113788b3760651220022fe8a2a1a8d3ace Mon Sep 17 00:00:00 2001 From: chardub Date: Sat, 7 Jun 2025 08:16:50 -0400 Subject: [PATCH] 6.6 update --- .gitmodules | 3 - Data/.DS_Store | Bin 61444 -> 61444 bytes Data/Scripts | 1 - Data/Scripts/001_Settings.rb | 646 + .../001_Debugging/001_PBDebug.rb | 40 + .../001_Debugging/002_DebugConsole.rb | 54 + .../001_Technical/001_Debugging/003_Errors.rb | 93 + .../001_Debugging/004_Validation.rb | 31 + .../001_Debugging/005_Deprecation.rb | 53 + .../001_Technical/001_MKXP_Compatibility.rb | 48 + .../001_Technical/002_Files/001_FileTests.rb | 492 + .../001_Technical/002_Files/002_FileMixins.rb | 150 + .../002_Files/003_HTTP_Utilities.rb | 150 + .../001_Technical/002_RubyUtilities.rb | 142 + .../001_Technical/003_Intl_Messages.rb | 789 + Data/Scripts/001_Technical/004_Input.rb | 33 + .../001_Technical/005_PluginManager.rb | 712 + Data/Scripts/001_Technical/006_RPG_Sprite.rb | 534 + Data/Scripts/002_BattleSettings.rb | 78 + Data/Scripts/002_Save data/001_SaveData.rb | 96 + .../002_Save data/002_SaveData_Value.rb | 269 + .../002_Save data/003_SaveData_Conversion.rb | 221 + .../002_Save data/004_Game_SaveValues.rb | 135 + .../002_Save data/005_Game_SaveConversions.rb | 242 + .../003_Game processing/001_StartGame.rb | 259 + .../003_Game processing/002_Scene_Map.rb | 292 + .../003_Game processing/003_Interpreter.rb | 457 + .../004_Interpreter_Commands.rb | 1027 + .../003_Game processing/005_Event_Handlers.rb | 275 + .../006_Event_OverworldEvents.rb | 172 + .../004_Game classes/001_Game_Screen.rb | 157 + .../001_Game_Temp.rb | 68 + .../002_Game_Switches.rb | 30 + .../003_Game_Variables.rb | 30 + .../004_Game_SelfSwitches.rb | 29 + .../004_Game classes/002_Game_System.rb | 286 + .../004_Game classes/003_Game_Picture.rb | 156 + Data/Scripts/004_Game classes/004_Game_Map.rb | 547 + .../005_Game_Map_Autoscroll.rb | 194 + .../004_Game classes/006_Game_MapFactory.rb | 526 + .../004_Game classes/007_Game_Character.rb | 1178 + .../004_Game classes/008_Game_Event.rb | 289 + .../004_Game classes/009_Game_Player.rb | 447 + .../010_Game_Player_Visuals.rb | 126 + .../004_Game classes/011_Game_CommonEvent.rb | 82 + .../012_Game_DependentEvents.rb | 588 + .../005_Map renderer/001_Tilemap_XP.rb | 921 + .../Scripts/005_Sprites/001_Sprite_Picture.rb | 58 + Data/Scripts/005_Sprites/002_Sprite_Timer.rb | 45 + .../005_Sprites/003_Sprite_Character.rb | 270 + .../005_Sprites/004_Sprite_Reflection.rb | 93 + .../005_Sprites/005_Sprite_SurfBase.rb | 150 + .../005_Sprites/006_Spriteset_Global.rb | 30 + Data/Scripts/005_Sprites/007_Spriteset_Map.rb | 168 + .../005_Sprites/008_Sprite_AnimationSprite.rb | 84 + .../005_Sprites/009_Sprite_DynamicShadows.rb | 253 + .../Scripts/005_Sprites/010_ParticleEngine.rb | 586 + Data/Scripts/005_Sprites/011_PictureEx.rb | 519 + Data/Scripts/005_Sprites/012_Interpolators.rb | 172 + .../005_Sprites/013_ScreenPosHelper.rb | 43 + .../005_Sprites/013_Sprite_Player_Offsets.rb | 35 + .../005_Sprites/013_Sprite_Wearable.rb | 170 + Data/Scripts/005_Sprites/014_Sprite_Hair.rb | 85 + Data/Scripts/005_Sprites/014_Sprite_Hat.rb | 8 + Data/Scripts/005_Sprites/016_Sprite_Player.rb | 122 + .../006_Map renderer/001_TilemapRenderer.rb | 763 + .../006_Map renderer/002_TilesetWrapper.rb | 97 + .../006_Map renderer/003_AutotileExpander.rb | 75 + .../006_Map renderer/004_TileDrawingHelper.rb | 246 + .../007_Objects and windows/001_RPG_Cache.rb | 167 + .../002_MessageConfig.rb | 818 + .../007_Objects and windows/003_Window.rb | 604 + .../004_SpriteWindow.rb | 936 + .../005_SpriteWindow_text.rb | 1401 + .../006_SpriteWindow_pictures.rb | 123 + .../007_SpriteWrapper.rb | 496 + .../008_AnimatedBitmap.rb | 360 + .../007_Objects and windows/009_Planes.rb | 230 + .../007_Objects and windows/010_DrawText.rb | 1277 + .../007_Objects and windows/011_Messages.rb | 1057 + .../007_Objects and windows/012_TextEntry.rb | 564 + Data/Scripts/008_Audio/001_Audio.rb | 171 + Data/Scripts/008_Audio/002_Audio_Play.rb | 294 + Data/Scripts/009_Scenes/001_Transitions.rb | 1627 + Data/Scripts/009_Scenes/002_EventScene.rb | 198 + Data/Scripts/010_Data/001_GameData.rb | 285 + .../001_Hardcoded data/001_GrowthRate.rb | 190 + .../001_Hardcoded data/002_GenderRatio.rb | 77 + .../001_Hardcoded data/003_EggGroup.rb | 102 + .../001_Hardcoded data/004_BodyShape.rb | 117 + .../001_Hardcoded data/005_BodyColor.rb | 90 + .../001_Hardcoded data/006_Habitat.rb | 76 + .../001_Hardcoded data/007_Evolution.rb | 599 + .../010_Data/001_Hardcoded data/008_Stat.rb | 131 + .../010_Data/001_Hardcoded data/009_Nature.rb | 200 + .../010_Data/001_Hardcoded data/010_Status.rb | 79 + .../001_Hardcoded data/011_TerrainTag.rb | 302 + .../001_Hardcoded data/012_Weather.rb | 194 + .../001_Hardcoded data/013_EncounterType.rb | 219 + .../001_Hardcoded data/014_Environment.rb | 131 + .../001_Hardcoded data/015_BattleWeather.rb | 81 + .../001_Hardcoded data/016_BattleTerrain.rb | 58 + .../010_Data/001_Hardcoded data/017_Target.rb | 194 + .../010_Data/002_PBS data/001_MiscPBSData.rb | 107 + .../002_PBS data/002_PhoneDatabase.rb | 31 + .../Scripts/010_Data/002_PBS data/003_Type.rb | 132 + .../010_Data/002_PBS data/004_Ability.rb | 31 + .../Scripts/010_Data/002_PBS data/005_Move.rb | 85 + .../Scripts/010_Data/002_PBS data/006_Item.rb | 324 + .../010_Data/002_PBS data/007_BerryPlant.rb | 36 + .../010_Data/002_PBS data/008_Species.rb | 444 + .../002_PBS data/009_Species_Files.rb | 388 + .../010_Data/002_PBS data/010_Ribbon.rb | 31 + .../010_Data/002_PBS data/011_Encounter.rb | 78 + .../002_PBS data/011_EncounterModern.rb | 77 + .../002_PBS data/011_Encounter_random.rb | 77 + .../010_Data/002_PBS data/012_TrainerType.rb | 157 + .../010_Data/002_PBS data/013_Trainer.rb | 442 + .../002_PBS data/013_TrainerExpert.rb | 14 + .../002_PBS data/013_TrainerModern.rb | 369 + .../010_Data/002_PBS data/014_Metadata.rb | 147 + .../010_Data/002_PBS data/015_MapMetadata.rb | 129 + .../001_Battler/001_PokeBattle_Battler.rb | 801 + .../001_Battler/002_Battler_Initialize.rb | 330 + .../001_Battler/003_Battler_ChangeSelf.rb | 312 + .../001_Battler/004_Battler_Statuses.rb | 576 + .../001_Battler/005_Battler_StatStages.rb | 308 + .../001_Battler/006_Battler_AbilityAndItem.rb | 303 + .../001_Battler/007_Battler_UseMove.rb | 809 + .../008_Battler_UseMove_Targeting.rb | 193 + .../009_Battler_UseMove_SuccessChecks.rb | 541 + .../010_Battler_UseMove_TriggerEffects.rb | 194 + Data/Scripts/011_Battle/001_PBEffects.rb | 182 + Data/Scripts/011_Battle/002_BattleHandlers.rb | 601 + .../002_Move/001_PokeBattle_Move.rb | 141 + .../011_Battle/002_Move/002_Move_Usage.rb | 350 + .../002_Move/003_Move_Usage_Calculations.rb | 492 + .../002_Move/004_Move_Effects_Generic.rb | 716 + .../002_Move/005_Move_Effects_000-07F.rb | 2572 ++ .../002_Move/006_Move_Effects_080-0FF.rb | 3723 ++ .../002_Move/007_Move_Effects_100-17F.rb | 2493 + .../003_Battle/001_PokeBattle_BattleCommon.rb | 264 + .../003_Battle/002_PokeBattle_Battle.rb | 799 + .../003_Battle/003_Battle_StartAndEnd.rb | 581 + .../004_Battle_ExpAndMoveLearning.rb | 302 + .../005_Battle_Action_AttacksPriority.rb | 248 + .../003_Battle/006_Battle_Action_Switching.rb | 415 + .../003_Battle/007_Battle_Action_UseItem.rb | 148 + .../003_Battle/008_Battle_Action_Running.rb | 156 + .../003_Battle/009_Battle_Action_Other.rb | 188 + .../003_Battle/010_Battle_Phase_Command.rb | 253 + .../003_Battle/011_Battle_Phase_Attack.rb | 190 + .../003_Battle/012_Battle_Phase_EndOfRound.rb | 666 + .../003_BattleHandlers_Abilities.rb | 2426 + .../011_Battle/004_AI/001_PokeBattle_AI.rb | 69 + Data/Scripts/011_Battle/004_AI/002_AI_Item.rb | 171 + .../011_Battle/004_AI/003_AI_Switch.rb | 178 + Data/Scripts/011_Battle/004_AI/004_AI_Move.rb | 291 + .../004_AI/005_AI_Move_EffectScores.rb | 3053 ++ .../004_AI/006_AI_Move_Utilities.rb | 669 + .../011_Battle/004_BattleHandlers_Items.rb | 1590 + .../005_BallHandlers_PokeBallEffects.rb | 251 + .../001_PokeBattle_Animation.rb | 278 + .../002_PokeBattle_SceneAnimations.rb | 876 + .../003_PokeBattle_SceneConstants.rb | 67 + .../004_PokeBattle_SceneElements.rb | 743 + .../005_PokeBattle_SceneMenus.rb | 552 + .../005_Battle scene/006_PokeBattle_Scene.rb | 351 + .../005_Battle scene/007_Scene_Initialize.rb | 269 + .../005_Battle scene/008_Scene_Commands.rb | 475 + .../005_Battle scene/009_Scene_Animations.rb | 575 + .../001_PokeBattle_AnimationPlayer.rb | 876 + .../002_PokeBattle_SafariZone.rb | 511 + .../003_PokeBattle_BugContest.rb | 87 + .../004_PokeBattle_BattlePalace.rb | 250 + .../005_PokeBattle_BattleArena.rb | 349 + .../006_PokeBattle_BattleRecord.rb | 307 + .../007_PokeBattle_DebugScene.rb | 84 + .../008_PokeBattle_BattlePeer.rb | 78 + .../009_PokeBattle_Clauses.rb | 242 + .../011_Battle/006_PokeBattle_ActiveField.rb | 90 + .../011_Battle/007_PokeBattle_DamageState.rb | 50 + .../001_Overworld_Weather.rb | 568 + .../002_Overworld_Overlays.rb | 291 + .../003_Overworld_MapTransitionAnims.rb | 151 + Data/Scripts/012_Overworld/001_Overworld.rb | 962 + .../001_Overworld_BattleStarting.rb | 860 + .../002_Overworld_BattleIntroAnim.rb | 352 + .../003_Overworld_WildEncounters.rb | 465 + .../004_Overworld_EncounterModifiers.rb | 50 + .../005_Overworld_RoamingPokemon.rb | 257 + .../012_Overworld/002_Overworld_Metadata.rb | 291 + .../012_Overworld/003_Overworld_Time.rb | 340 + .../012_Overworld/004_Overworld_FieldMoves.rb | 1221 + .../012_Overworld/005_Overworld_Fishing.rb | 174 + .../006_Overworld_BerryPlants.rb | 553 + .../012_Overworld/007_Overworld_DayCare.rb | 403 + .../008_Overworld_RandomDungeons.rb | 669 + Data/Scripts/013_Items/001_Item_Utilities.rb | 794 + Data/Scripts/013_Items/002_Item_Effects.rb | 1098 + .../013_Items/003_Item_BattleEffects.rb | 695 + Data/Scripts/013_Items/004_1_PokeradarUI.rb | 138 + Data/Scripts/013_Items/004_Item_Phone.rb | 303 + Data/Scripts/013_Items/005_Item_PokeRadar.rb | 345 + Data/Scripts/013_Items/006_Item_Mail.rb | 134 + Data/Scripts/013_Items/007_Item_Sprites.rb | 163 + Data/Scripts/013_Items/008_PokemonBag.rb | 400 + .../001_Pokemon-related/001_FormHandlers.rb | 609 + .../002_ShadowPokemon_Other.rb | 529 + .../003_Pokemon_Sprites.rb | 424 + .../001_Pokemon-related/004_PokemonStorage.rb | 406 + Data/Scripts/014_Pokemon/001_Pokemon.rb | 1617 + .../014_Pokemon/002_Pokemon_MegaEvolution.rb | 80 + .../014_Pokemon/003_Pokemon_ShadowPokemon.rb | 160 + Data/Scripts/014_Pokemon/004_Pokemon_Move.rb | 78 + Data/Scripts/014_Pokemon/005_Pokemon_Owner.rb | 78 + .../014_Pokemon/006_Pokemon_Deprecated.rb | 207 + .../015_Trainers and player/001_Trainer.rb | 243 + .../002_Trainer_LoadAndNew.rb | 145 + .../003_Trainer_Sprites.rb | 84 + .../015_Trainers and player/004_Player.rb | 391 + .../005_Player_Pokedex.rb | 487 + .../006_Player_Deprecated.rb | 194 + .../001_UI_SplashesAndTitleScreen.rb | 122 + .../001_Non-interactive UI/002_UI_Controls.rb | 80 + .../003_UI_EggHatching.rb | 248 + .../004_UI_Evolution.rb | 666 + .../001_Non-interactive UI/005_UI_Trading.rb | 254 + .../006_UI_HallOfFame.rb | 673 + .../001_Non-interactive UI/007_UI_Credits.rb | 374 + Data/Scripts/016_UI/001_UI_PauseMenu.rb | 289 + Data/Scripts/016_UI/002_UI_Pokedex_Menu.rb | 127 + Data/Scripts/016_UI/003_UI_Pokedex_Main.rb | 1233 + Data/Scripts/016_UI/004_UI_Pokedex_Entry.rb | 825 + Data/Scripts/016_UI/005_UI_Party.rb | 1589 + Data/Scripts/016_UI/006_UI_Summary.rb | 1537 + Data/Scripts/016_UI/007_UI_Bag.rb | 716 + Data/Scripts/016_UI/008_UI_Pokegear.rb | 157 + Data/Scripts/016_UI/009_UI_RegionMap.rb | 376 + Data/Scripts/016_UI/010_UI_Phone.rb | 149 + Data/Scripts/016_UI/011_UI_Jukebox.rb | 136 + Data/Scripts/016_UI/012_UI_TrainerCard.rb | 161 + Data/Scripts/016_UI/013_UI_Load.rb | 374 + Data/Scripts/016_UI/014_UI_Save.rb | 129 + Data/Scripts/016_UI/015_UI_Options.rb | 533 + Data/Scripts/016_UI/016_UI_ReadyMenu.rb | 327 + Data/Scripts/016_UI/017_UI_PokemonStorage.rb | 2402 + Data/Scripts/016_UI/018_UI_ItemStorage.rb | 368 + Data/Scripts/016_UI/019_UI_PC.rb | 275 + Data/Scripts/016_UI/020_UI_PokeMart.rb | 794 + Data/Scripts/016_UI/021_UI_MoveRelearner.rb | 217 + Data/Scripts/016_UI/022_UI_PurifyChamber.rb | 1361 + Data/Scripts/016_UI/023_UI_MysteryGift.rb | 421 + Data/Scripts/016_UI/024_UI_TextEntry.rb | 804 + .../017_Minigames/001_Minigame_Duel.rb | 391 + .../017_Minigames/002_Minigame_TripleTriad.rb | 1302 + .../017_Minigames/003_Minigame_SlotMachine.rb | 404 + .../017_Minigames/004_Minigame_VoltorbFlip.rb | 626 + .../017_Minigames/005_Minigame_Lottery.rb | 54 + .../017_Minigames/006_Minigame_Mining.rb | 622 + .../017_Minigames/007_Minigame_TilePuzzles.rb | 582 + .../001_Challenge_BattleChallenge.rb | 428 + .../001_Battle Frontier/002_Challenge_Data.rb | 316 + .../003_Challenge_ChooseFoes.rb | 188 + .../004_Challenge_Battles.rb | 146 + .../001_Battle Frontier/005_UI_BattleSwap.rb | 230 + .../001_SafariZone.rb | 145 + .../001_Challenge_ChallengeRules.rb | 382 + .../002_Challenge_Rulesets.rb | 329 + .../003_Challenge_EntryRestrictions.rb | 398 + .../004_Challenge_LevelAdjustment.rb | 227 + .../005_Challenge_BattleRules.rb | 97 + .../002_BugContest.rb | 398 + .../001_ChallengeGenerator_Data.rb | 344 + .../002_ChallengeGenerator_Pokemon.rb | 366 + .../003_ChallengeGenerator_Trainers.rb | 215 + .../004_ChallengeGenerator_BattleSim.rb | 433 + Data/Scripts/019_Utilities/001_Utilities.rb | 604 + .../019_Utilities/002_Utilities_Pokemon.rb | 279 + .../003_Utilities_BattleAudio.rb | 146 + .../001_Editor screens/001_EditorScreens.rb | 320 + .../002_EditorScreens_TerrainTags.rb | 234 + .../003_EditorScreens_MapConnections.rb | 590 + .../004_EditorScreens_SpritePositioning.rb | 421 + .../Scripts/020_Debug/001_Editor_Utilities.rb | 417 + .../001_AnimEditor_SceneElements.rb | 1026 + .../002_AnimEditor_ControlsButtons.rb | 910 + .../003_AnimEditor_Interpolation.rb | 440 + .../004_AnimEditor_ExportImport.rb | 144 + .../005_AnimEditor_Functions.rb | 1199 + .../Scripts/020_Debug/002_Editor_DataTypes.rb | 1535 + .../003_Debug menus/001_Debug_Menus.rb | 185 + .../003_Debug menus/002_Debug_MenuCommands.rb | 1053 + .../003_Debug_MenuExtraCode.rb | 904 + .../004_Debug_MenuSpriteRenamer.rb | 311 + .../005_Debug_PokemonCommands.rb | 1316 + Data/Scripts/020_Debug/003_Editor_Listers.rb | 609 + Data/Scripts/021_Compiler/001_Compiler.rb | 852 + .../021_Compiler/002_Compiler_CompilePBS.rb | 1653 + .../021_Compiler/003_Compiler_WritePBS.rb | 805 + .../004_Compiler_MapsAndEvents.rb | 1433 + Data/Scripts/025-Randomizer/Random Pokemon.rb | 198 + .../025-Randomizer/RandomizerSettings.rb | 535 + .../Scripts/025-Randomizer/RandomizerUtils.rb | 126 + .../025-Randomizer/randomizer - encounters.rb | 223 + .../randomizer gym leader edit.rb | 773 + Data/Scripts/025-Randomizer/randomizer.rb | 366 + .../Scripts/048_Fusion/DoublePreviewScreen.rb | 222 + Data/Scripts/048_Fusion/FusedSpecies.rb | 432 + Data/Scripts/048_Fusion/FusionAnim.rb | 38 + Data/Scripts/048_Fusion/FusionMenu.rb | 115 + Data/Scripts/048_Fusion/FusionMovesMenu.rb | 240 + .../Scripts/048_Fusion/FusionPreviewScreen.rb | 50 + Data/Scripts/048_Fusion/PokemonFusion.rb | 1143 + Data/Scripts/048_Fusion/SplitNames.rb | 1229 + .../Sprites/001_PifSpriteBitmapCache.rb | 52 + .../Sprites/002_PIFSpriteExtracter.rb | 98 + .../048_Fusion/Sprites/AutogenExtracter.rb | 160 + .../048_Fusion/Sprites/BaseSpriteExtracter.rb | 61 + .../048_Fusion/Sprites/BattleSpriteLoader.rb | 279 + .../Sprites/CustomSpriteExtracter.rb | 106 + Data/Scripts/048_Fusion/Sprites/PIFSprite.rb | 122 + .../Sprites/SpritesSubstitutions.rb | 89 + Data/Scripts/049_Compatibility/Constants.rb | 352 + .../049_Compatibility/DeprecatedClasses.rb | 2 + Data/Scripts/049_Compatibility/EggGroups.rb | 0 .../049_Compatibility/MarinUtilities.rb | 1291 + Data/Scripts/049_Compatibility/PBItems.rb | 662 + Data/Scripts/049_Compatibility/PBMoves.rb | 684 + Data/Scripts/049_Compatibility/PBSpecies.rb | 30 + Data/Scripts/049_Compatibility/PBTrainers.rb | 120 + .../049_Compatibility/UtilityMethods.rb | 187 + Data/Scripts/049_Compatibility/pb_types.rb | 7 + .../001_OutfitsMain/LayeredClothes.rb | 409 + .../001_OutfitsMain/OutfitSelector.rb | 174 + .../001_OutfitsMain/OutfitsGlobal.rb | 75 + .../001_OutfitsMain/OutfitsSearch.rb | 221 + Data/Scripts/050_Outfits/ItemSets.rb | 185 + Data/Scripts/050_Outfits/OutfitIds.rb | 161 + .../050_Outfits/UI/CharacterSelectMenu.rb | 181 + .../UI/CharacterSelectMenuPresenter.rb | 276 + .../050_Outfits/UI/LayeredClothes_Menus.rb | 265 + .../UI/PokemonHatScreenPresenter.rb | 103 + .../050_Outfits/UI/PokemonHatScreenView.rb | 139 + .../050_Outfits/UI/TrainerClothesPreview.rb | 72 + .../UI/clothesShop/0_OutfitsMartAdapter.rb | 154 + .../UI/clothesShop/ClothesMartAdapter.rb | 108 + .../050_Outfits/UI/clothesShop/ClothesShop.rb | 146 + .../UI/clothesShop/ClothesShopPresenter.rb | 163 + .../ClothesShopPresenter_HatsMenu.rb | 154 + .../UI/clothesShop/ClothesShopView.rb | 205 + .../UI/clothesShop/HairMartAdapter.rb | 200 + .../UI/clothesShop/HairShopPresenter.rb | 68 + .../050_Outfits/UI/clothesShop/HatShopView.rb | 110 + .../UI/clothesShop/HatsMartAdapter.rb | 222 + .../UI/hairMenu/HairStyleSelectionMenuView.rb | 160 + .../HairstyleSelectionMenuPresenter.rb | 187 + .../050_Outfits/utils/OutfitFilenameUtils.rb | 126 + .../050_Outfits/utils/OutfitsGameplayUtils.rb | 436 + .../050_Outfits/wrappers/001_Outfit.rb | 42 + Data/Scripts/050_Outfits/wrappers/Clothes.rb | 11 + .../Scripts/050_Outfits/wrappers/Hairstyle.rb | 12 + Data/Scripts/050_Outfits/wrappers/Hat.rb | 11 + Data/Scripts/051_Wrappers/quest_reward.rb | 15 + Data/Scripts/051_Wrappers/type_expert.rb | 158 + .../052_InfiniteFusion/AttributeReader.rb | 0 Data/Scripts/052_InfiniteFusion/Autosave.rb | 133 + .../052_InfiniteFusion/BattleLounge.rb | 232 + .../052_InfiniteFusion/BetterRegionMap.rb | 988 + .../052_InfiniteFusion/CustomTrainers.rb | 232 + Data/Scripts/052_InfiniteFusion/DevUtils.rb | 191 + .../Scripts/052_InfiniteFusion/DisplayText.rb | 51 + .../052_InfiniteFusion/DoubleAbilities.rb | 259 + .../DoubleAbilitiesHandlersOverrides.rb | 389 + .../052_InfiniteFusion/DoubleAbilities_UI.rb | 130 + .../052_InfiniteFusion/EggMoveTutor.rb | 32 + .../052_InfiniteFusion/ExperimentalOptions.rb | 72 + .../052_InfiniteFusion/ExportScripts.rb | 38 + Data/Scripts/052_InfiniteFusion/Footprints.rb | 225 + .../052_InfiniteFusion/FusionMoveTutor.rb | 205 + .../052_InfiniteFusion/FusionSprites.rb | 412 + .../Scripts/052_InfiniteFusion/FusionUtils.rb | 478 + .../Scripts/052_InfiniteFusion/GameOptions.rb | 262 + .../052_InfiniteFusion/GameplayUtils.rb | 1875 + Data/Scripts/052_InfiniteFusion/Gen 2.rb | 302 + .../052_InfiniteFusion/GeneralUtils.rb | 587 + .../052_InfiniteFusion/GuessPokemonQuiz.rb | 309 + .../052_InfiniteFusion/HiddenAbilityMaps.rb | 47 + Data/Scripts/052_InfiniteFusion/HttpCalls.rb | 440 + .../ImprovedShinies/000_Shinies_Credits.rb | 2 + .../ImprovedShinies/SHINY_COLOR_OFFSETS.rb | 503 + .../ImprovedShinies/Shinies_AltSprites.rb | 20 + .../ImprovedShinies/Shinies_AnimatedBitmap.rb | 46 + .../ImprovedShinies/Shinies_Bitmap.rb | 153 + .../ImprovedShinies/Shinies_Pokedex.rb | 174 + .../ImprovedShinies/Shinies_PokemonIcons.rb | 41 + .../ImprovedShinies/Shinies_PokemonStorage.rb | 60 + .../ImprovedShinies/Shinies_Species.rb | 53 + .../Scripts/052_InfiniteFusion/IntroScreen.rb | 495 + .../Scripts/052_InfiniteFusion/MapExporter.rb | 189 + Data/Scripts/052_InfiniteFusion/Movie.rb | 79 + Data/Scripts/052_InfiniteFusion/MultiSaves.rb | 935 + Data/Scripts/052_InfiniteFusion/New Balls.rb | 128 + Data/Scripts/052_InfiniteFusion/New HMs.rb | 141 + .../052_InfiniteFusion/New Items effects.rb | 2077 + .../052_InfiniteFusion/NonMoneyShop.rb | 53 + .../052_InfiniteFusion/OnlineWondertrade.rb | 91 + Data/Scripts/052_InfiniteFusion/Overrides.rb | 3 + .../052_InfiniteFusion/OverworldShadows.rb | 302 + .../Scripts/052_InfiniteFusion/Pathfinding.rb | 89 + .../052_InfiniteFusion/PokedexUtils.rb | 89 + .../052_InfiniteFusion/PokemonSelection.rb | 137 + .../052_InfiniteFusion/Quests/PoliceQuest.rb | 8 + .../052_InfiniteFusion/Quests/QuestIcons.rb | 201 + .../Quests/QuestLogScript.rb | 1109 + .../052_InfiniteFusion/Quests/Quests.rb | 136 + .../052_InfiniteFusion/Quests/TRQuests.rb | 380 + .../052_InfiniteFusion/RandomAddOns.rb | 19 + Data/Scripts/052_InfiniteFusion/Silhouette.rb | 149 + .../052_InfiniteFusion/SpeechBubbles.rb | 217 + Data/Scripts/052_InfiniteFusion/Spped Up.rb | 71 + .../052_InfiniteFusion/SpriteCreditsUtils.rb | 498 + Data/Scripts/052_InfiniteFusion/TeamFlags.rb | 18 + Data/Scripts/052_InfiniteFusion/TempEvents.rb | 34 + .../TrainerCardBackgrounds.rb | 196 + .../TrainerGeneratorUtils.rb | 36 + .../052_InfiniteFusion/Trainers Rebattle.rb | 295 + .../052_InfiniteFusion/TripleFusion.rb | 90 + .../UI_Pokedex_SpritesPage.rb | 377 + Data/Scripts/052_InfiniteFusion/UnrealTime.rb | 293 + .../Scripts/052_InfiniteFusion/WaterEffect.rb | 23 + .../052_InfiniteFusion/WonderTrade_names.rb | 1070 + .../Scripts/052_InfiniteFusion/Wondertrade.rb | 208 + .../052_InfiniteFusion/dynamic_waterfall.rb | 225 + .../052_InfiniteFusion/k_scriptsUtils.rb | 1167 + .../052_InfiniteFusion/mapExporter2.rb | 533 + .../052_InfiniteFusion/mapExporter2_ui.rb | 74 + Data/Scripts/052_InfiniteFusion/platform.rb | 251 + Data/Scripts/052_Tests/FusionUtilsTests.rb | 20 + Data/Scripts/053_PIF_Hoenn/DiveGraphics.rb | 51 + .../DynamicWeather_Encounters.rb | 42 + .../DynamicWeather_EncountersMethods.rb | 76 + .../DynamicWeather_MapLayout.rb | 96 + .../DynamicWeather_Overworld.rb | 29 + .../DynamicWeather/DynamicWeather_TownMap.rb | 131 + .../DynamicWeather/DynamicWeather_Utils.rb | 44 + .../DynamicWeather/DynamicWeather_Weather.rb | 376 + .../053_PIF_Hoenn/HoennGameplayUtils.rb | 27 + Data/Scripts/053_PIF_Hoenn/Hoenn_Rival.rb | 248 + .../Scripts/053_PIF_Hoenn/OverworldPokemon.rb | 15 + .../053_PIF_Hoenn/PokemartMapTransfers.rb | 137 + Data/Scripts/053_PIF_Hoenn/SurfingSplashes.rb | 160 + .../01_battled_trainer_random_event.rb | 33 + .../TrainerRematches/02_battled_trainer.rb | 217 + .../03_TrainerRematch_System.rb | 70 + .../04_TrainerRematch_Battle.rb | 123 + .../StartersSelectionScene.rb | 203 + .../TrainerRematch_FAVORITE_TYPES.rb | 85 + .../TrainerRematch_FRIENDSHIP.rb | 85 + .../TrainerRematches/TrainerRematch_Menus.rb | 118 + .../TrainerRematch_Partner.rb | 13 + .../TrainerRematch_RandomEvents.rb | 195 + .../TrainerRematches/TrainerRematch_Trade.rb | 273 + .../WeatherSystem/Configuration.rb | 162 + Data/Scripts/999_Main/999_Main.rb | 198 + Data/Scripts/DownloadedSettings.rb | 51 + Data/pokedex/dex.json | 37559 +--------------- 467 files changed, 171196 insertions(+), 36566 deletions(-) delete mode 100644 .gitmodules delete mode 160000 Data/Scripts create mode 100644 Data/Scripts/001_Settings.rb create mode 100644 Data/Scripts/001_Technical/001_Debugging/001_PBDebug.rb create mode 100644 Data/Scripts/001_Technical/001_Debugging/002_DebugConsole.rb create mode 100644 Data/Scripts/001_Technical/001_Debugging/003_Errors.rb create mode 100644 Data/Scripts/001_Technical/001_Debugging/004_Validation.rb create mode 100644 Data/Scripts/001_Technical/001_Debugging/005_Deprecation.rb create mode 100644 Data/Scripts/001_Technical/001_MKXP_Compatibility.rb create mode 100644 Data/Scripts/001_Technical/002_Files/001_FileTests.rb create mode 100644 Data/Scripts/001_Technical/002_Files/002_FileMixins.rb create mode 100644 Data/Scripts/001_Technical/002_Files/003_HTTP_Utilities.rb create mode 100644 Data/Scripts/001_Technical/002_RubyUtilities.rb create mode 100644 Data/Scripts/001_Technical/003_Intl_Messages.rb create mode 100644 Data/Scripts/001_Technical/004_Input.rb create mode 100644 Data/Scripts/001_Technical/005_PluginManager.rb create mode 100644 Data/Scripts/001_Technical/006_RPG_Sprite.rb create mode 100644 Data/Scripts/002_BattleSettings.rb create mode 100644 Data/Scripts/002_Save data/001_SaveData.rb create mode 100644 Data/Scripts/002_Save data/002_SaveData_Value.rb create mode 100644 Data/Scripts/002_Save data/003_SaveData_Conversion.rb create mode 100644 Data/Scripts/002_Save data/004_Game_SaveValues.rb create mode 100644 Data/Scripts/002_Save data/005_Game_SaveConversions.rb create mode 100644 Data/Scripts/003_Game processing/001_StartGame.rb create mode 100644 Data/Scripts/003_Game processing/002_Scene_Map.rb create mode 100644 Data/Scripts/003_Game processing/003_Interpreter.rb create mode 100644 Data/Scripts/003_Game processing/004_Interpreter_Commands.rb create mode 100644 Data/Scripts/003_Game processing/005_Event_Handlers.rb create mode 100644 Data/Scripts/003_Game processing/006_Event_OverworldEvents.rb create mode 100644 Data/Scripts/004_Game classes/001_Game_Screen.rb create mode 100644 Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb create mode 100644 Data/Scripts/004_Game classes/001_Switches and Variables/002_Game_Switches.rb create mode 100644 Data/Scripts/004_Game classes/001_Switches and Variables/003_Game_Variables.rb create mode 100644 Data/Scripts/004_Game classes/001_Switches and Variables/004_Game_SelfSwitches.rb create mode 100644 Data/Scripts/004_Game classes/002_Game_System.rb create mode 100644 Data/Scripts/004_Game classes/003_Game_Picture.rb create mode 100644 Data/Scripts/004_Game classes/004_Game_Map.rb create mode 100644 Data/Scripts/004_Game classes/005_Game_Map_Autoscroll.rb create mode 100644 Data/Scripts/004_Game classes/006_Game_MapFactory.rb create mode 100644 Data/Scripts/004_Game classes/007_Game_Character.rb create mode 100644 Data/Scripts/004_Game classes/008_Game_Event.rb create mode 100644 Data/Scripts/004_Game classes/009_Game_Player.rb create mode 100644 Data/Scripts/004_Game classes/010_Game_Player_Visuals.rb create mode 100644 Data/Scripts/004_Game classes/011_Game_CommonEvent.rb create mode 100644 Data/Scripts/004_Game classes/012_Game_DependentEvents.rb create mode 100644 Data/Scripts/005_Map renderer/001_Tilemap_XP.rb create mode 100644 Data/Scripts/005_Sprites/001_Sprite_Picture.rb create mode 100644 Data/Scripts/005_Sprites/002_Sprite_Timer.rb create mode 100644 Data/Scripts/005_Sprites/003_Sprite_Character.rb create mode 100644 Data/Scripts/005_Sprites/004_Sprite_Reflection.rb create mode 100644 Data/Scripts/005_Sprites/005_Sprite_SurfBase.rb create mode 100644 Data/Scripts/005_Sprites/006_Spriteset_Global.rb create mode 100644 Data/Scripts/005_Sprites/007_Spriteset_Map.rb create mode 100644 Data/Scripts/005_Sprites/008_Sprite_AnimationSprite.rb create mode 100644 Data/Scripts/005_Sprites/009_Sprite_DynamicShadows.rb create mode 100644 Data/Scripts/005_Sprites/010_ParticleEngine.rb create mode 100644 Data/Scripts/005_Sprites/011_PictureEx.rb create mode 100644 Data/Scripts/005_Sprites/012_Interpolators.rb create mode 100644 Data/Scripts/005_Sprites/013_ScreenPosHelper.rb create mode 100644 Data/Scripts/005_Sprites/013_Sprite_Player_Offsets.rb create mode 100644 Data/Scripts/005_Sprites/013_Sprite_Wearable.rb create mode 100644 Data/Scripts/005_Sprites/014_Sprite_Hair.rb create mode 100644 Data/Scripts/005_Sprites/014_Sprite_Hat.rb create mode 100644 Data/Scripts/005_Sprites/016_Sprite_Player.rb create mode 100644 Data/Scripts/006_Map renderer/001_TilemapRenderer.rb create mode 100644 Data/Scripts/006_Map renderer/002_TilesetWrapper.rb create mode 100644 Data/Scripts/006_Map renderer/003_AutotileExpander.rb create mode 100644 Data/Scripts/006_Map renderer/004_TileDrawingHelper.rb create mode 100644 Data/Scripts/007_Objects and windows/001_RPG_Cache.rb create mode 100644 Data/Scripts/007_Objects and windows/002_MessageConfig.rb create mode 100644 Data/Scripts/007_Objects and windows/003_Window.rb create mode 100644 Data/Scripts/007_Objects and windows/004_SpriteWindow.rb create mode 100644 Data/Scripts/007_Objects and windows/005_SpriteWindow_text.rb create mode 100644 Data/Scripts/007_Objects and windows/006_SpriteWindow_pictures.rb create mode 100644 Data/Scripts/007_Objects and windows/007_SpriteWrapper.rb create mode 100644 Data/Scripts/007_Objects and windows/008_AnimatedBitmap.rb create mode 100644 Data/Scripts/007_Objects and windows/009_Planes.rb create mode 100644 Data/Scripts/007_Objects and windows/010_DrawText.rb create mode 100644 Data/Scripts/007_Objects and windows/011_Messages.rb create mode 100644 Data/Scripts/007_Objects and windows/012_TextEntry.rb create mode 100644 Data/Scripts/008_Audio/001_Audio.rb create mode 100644 Data/Scripts/008_Audio/002_Audio_Play.rb create mode 100644 Data/Scripts/009_Scenes/001_Transitions.rb create mode 100644 Data/Scripts/009_Scenes/002_EventScene.rb create mode 100644 Data/Scripts/010_Data/001_GameData.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/001_GrowthRate.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/002_GenderRatio.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/003_EggGroup.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/004_BodyShape.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/005_BodyColor.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/006_Habitat.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/007_Evolution.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/008_Stat.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/009_Nature.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/010_Status.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/011_TerrainTag.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/012_Weather.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/013_EncounterType.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/014_Environment.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/015_BattleWeather.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/016_BattleTerrain.rb create mode 100644 Data/Scripts/010_Data/001_Hardcoded data/017_Target.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/001_MiscPBSData.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/002_PhoneDatabase.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/003_Type.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/004_Ability.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/005_Move.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/006_Item.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/007_BerryPlant.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/008_Species.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/009_Species_Files.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/010_Ribbon.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/011_Encounter.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/011_EncounterModern.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/011_Encounter_random.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/012_TrainerType.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/013_Trainer.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/013_TrainerExpert.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/013_TrainerModern.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/014_Metadata.rb create mode 100644 Data/Scripts/010_Data/002_PBS data/015_MapMetadata.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/001_PokeBattle_Battler.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/002_Battler_Initialize.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/003_Battler_ChangeSelf.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/004_Battler_Statuses.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/005_Battler_StatStages.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/006_Battler_AbilityAndItem.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/007_Battler_UseMove.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/008_Battler_UseMove_Targeting.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/009_Battler_UseMove_SuccessChecks.rb create mode 100644 Data/Scripts/011_Battle/001_Battler/010_Battler_UseMove_TriggerEffects.rb create mode 100644 Data/Scripts/011_Battle/001_PBEffects.rb create mode 100644 Data/Scripts/011_Battle/002_BattleHandlers.rb create mode 100644 Data/Scripts/011_Battle/002_Move/001_PokeBattle_Move.rb create mode 100644 Data/Scripts/011_Battle/002_Move/002_Move_Usage.rb create mode 100644 Data/Scripts/011_Battle/002_Move/003_Move_Usage_Calculations.rb create mode 100644 Data/Scripts/011_Battle/002_Move/004_Move_Effects_Generic.rb create mode 100644 Data/Scripts/011_Battle/002_Move/005_Move_Effects_000-07F.rb create mode 100644 Data/Scripts/011_Battle/002_Move/006_Move_Effects_080-0FF.rb create mode 100644 Data/Scripts/011_Battle/002_Move/007_Move_Effects_100-17F.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/001_PokeBattle_BattleCommon.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/002_PokeBattle_Battle.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/003_Battle_StartAndEnd.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/004_Battle_ExpAndMoveLearning.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/005_Battle_Action_AttacksPriority.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/006_Battle_Action_Switching.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/007_Battle_Action_UseItem.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/008_Battle_Action_Running.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/009_Battle_Action_Other.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/010_Battle_Phase_Command.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/011_Battle_Phase_Attack.rb create mode 100644 Data/Scripts/011_Battle/003_Battle/012_Battle_Phase_EndOfRound.rb create mode 100644 Data/Scripts/011_Battle/003_BattleHandlers_Abilities.rb create mode 100644 Data/Scripts/011_Battle/004_AI/001_PokeBattle_AI.rb create mode 100644 Data/Scripts/011_Battle/004_AI/002_AI_Item.rb create mode 100644 Data/Scripts/011_Battle/004_AI/003_AI_Switch.rb create mode 100644 Data/Scripts/011_Battle/004_AI/004_AI_Move.rb create mode 100644 Data/Scripts/011_Battle/004_AI/005_AI_Move_EffectScores.rb create mode 100644 Data/Scripts/011_Battle/004_AI/006_AI_Move_Utilities.rb create mode 100644 Data/Scripts/011_Battle/004_BattleHandlers_Items.rb create mode 100644 Data/Scripts/011_Battle/005_BallHandlers_PokeBallEffects.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/001_PokeBattle_Animation.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/002_PokeBattle_SceneAnimations.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/003_PokeBattle_SceneConstants.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/004_PokeBattle_SceneElements.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/005_PokeBattle_SceneMenus.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/006_PokeBattle_Scene.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/007_Scene_Initialize.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/008_Scene_Commands.rb create mode 100644 Data/Scripts/011_Battle/005_Battle scene/009_Scene_Animations.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/001_PokeBattle_AnimationPlayer.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/002_PokeBattle_SafariZone.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/003_PokeBattle_BugContest.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/004_PokeBattle_BattlePalace.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/005_PokeBattle_BattleArena.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/006_PokeBattle_BattleRecord.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/007_PokeBattle_DebugScene.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/008_PokeBattle_BattlePeer.rb create mode 100644 Data/Scripts/011_Battle/006_Other battle types/009_PokeBattle_Clauses.rb create mode 100644 Data/Scripts/011_Battle/006_PokeBattle_ActiveField.rb create mode 100644 Data/Scripts/011_Battle/007_PokeBattle_DamageState.rb create mode 100644 Data/Scripts/012_Overworld/001_Overworld visuals/001_Overworld_Weather.rb create mode 100644 Data/Scripts/012_Overworld/001_Overworld visuals/002_Overworld_Overlays.rb create mode 100644 Data/Scripts/012_Overworld/001_Overworld visuals/003_Overworld_MapTransitionAnims.rb create mode 100644 Data/Scripts/012_Overworld/001_Overworld.rb create mode 100644 Data/Scripts/012_Overworld/002_Battle triggering/001_Overworld_BattleStarting.rb create mode 100644 Data/Scripts/012_Overworld/002_Battle triggering/002_Overworld_BattleIntroAnim.rb create mode 100644 Data/Scripts/012_Overworld/002_Battle triggering/003_Overworld_WildEncounters.rb create mode 100644 Data/Scripts/012_Overworld/002_Battle triggering/004_Overworld_EncounterModifiers.rb create mode 100644 Data/Scripts/012_Overworld/002_Battle triggering/005_Overworld_RoamingPokemon.rb create mode 100644 Data/Scripts/012_Overworld/002_Overworld_Metadata.rb create mode 100644 Data/Scripts/012_Overworld/003_Overworld_Time.rb create mode 100644 Data/Scripts/012_Overworld/004_Overworld_FieldMoves.rb create mode 100644 Data/Scripts/012_Overworld/005_Overworld_Fishing.rb create mode 100644 Data/Scripts/012_Overworld/006_Overworld_BerryPlants.rb create mode 100644 Data/Scripts/012_Overworld/007_Overworld_DayCare.rb create mode 100644 Data/Scripts/012_Overworld/008_Overworld_RandomDungeons.rb create mode 100644 Data/Scripts/013_Items/001_Item_Utilities.rb create mode 100644 Data/Scripts/013_Items/002_Item_Effects.rb create mode 100644 Data/Scripts/013_Items/003_Item_BattleEffects.rb create mode 100644 Data/Scripts/013_Items/004_1_PokeradarUI.rb create mode 100644 Data/Scripts/013_Items/004_Item_Phone.rb create mode 100644 Data/Scripts/013_Items/005_Item_PokeRadar.rb create mode 100644 Data/Scripts/013_Items/006_Item_Mail.rb create mode 100644 Data/Scripts/013_Items/007_Item_Sprites.rb create mode 100644 Data/Scripts/013_Items/008_PokemonBag.rb create mode 100644 Data/Scripts/014_Pokemon/001_Pokemon-related/001_FormHandlers.rb create mode 100644 Data/Scripts/014_Pokemon/001_Pokemon-related/002_ShadowPokemon_Other.rb create mode 100644 Data/Scripts/014_Pokemon/001_Pokemon-related/003_Pokemon_Sprites.rb create mode 100644 Data/Scripts/014_Pokemon/001_Pokemon-related/004_PokemonStorage.rb create mode 100644 Data/Scripts/014_Pokemon/001_Pokemon.rb create mode 100644 Data/Scripts/014_Pokemon/002_Pokemon_MegaEvolution.rb create mode 100644 Data/Scripts/014_Pokemon/003_Pokemon_ShadowPokemon.rb create mode 100644 Data/Scripts/014_Pokemon/004_Pokemon_Move.rb create mode 100644 Data/Scripts/014_Pokemon/005_Pokemon_Owner.rb create mode 100644 Data/Scripts/014_Pokemon/006_Pokemon_Deprecated.rb create mode 100644 Data/Scripts/015_Trainers and player/001_Trainer.rb create mode 100644 Data/Scripts/015_Trainers and player/002_Trainer_LoadAndNew.rb create mode 100644 Data/Scripts/015_Trainers and player/003_Trainer_Sprites.rb create mode 100644 Data/Scripts/015_Trainers and player/004_Player.rb create mode 100644 Data/Scripts/015_Trainers and player/005_Player_Pokedex.rb create mode 100644 Data/Scripts/015_Trainers and player/006_Player_Deprecated.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/001_UI_SplashesAndTitleScreen.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/002_UI_Controls.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/003_UI_EggHatching.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/004_UI_Evolution.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/005_UI_Trading.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/006_UI_HallOfFame.rb create mode 100644 Data/Scripts/016_UI/001_Non-interactive UI/007_UI_Credits.rb create mode 100644 Data/Scripts/016_UI/001_UI_PauseMenu.rb create mode 100644 Data/Scripts/016_UI/002_UI_Pokedex_Menu.rb create mode 100644 Data/Scripts/016_UI/003_UI_Pokedex_Main.rb create mode 100644 Data/Scripts/016_UI/004_UI_Pokedex_Entry.rb create mode 100644 Data/Scripts/016_UI/005_UI_Party.rb create mode 100644 Data/Scripts/016_UI/006_UI_Summary.rb create mode 100644 Data/Scripts/016_UI/007_UI_Bag.rb create mode 100644 Data/Scripts/016_UI/008_UI_Pokegear.rb create mode 100644 Data/Scripts/016_UI/009_UI_RegionMap.rb create mode 100644 Data/Scripts/016_UI/010_UI_Phone.rb create mode 100644 Data/Scripts/016_UI/011_UI_Jukebox.rb create mode 100644 Data/Scripts/016_UI/012_UI_TrainerCard.rb create mode 100644 Data/Scripts/016_UI/013_UI_Load.rb create mode 100644 Data/Scripts/016_UI/014_UI_Save.rb create mode 100644 Data/Scripts/016_UI/015_UI_Options.rb create mode 100644 Data/Scripts/016_UI/016_UI_ReadyMenu.rb create mode 100644 Data/Scripts/016_UI/017_UI_PokemonStorage.rb create mode 100644 Data/Scripts/016_UI/018_UI_ItemStorage.rb create mode 100644 Data/Scripts/016_UI/019_UI_PC.rb create mode 100644 Data/Scripts/016_UI/020_UI_PokeMart.rb create mode 100644 Data/Scripts/016_UI/021_UI_MoveRelearner.rb create mode 100644 Data/Scripts/016_UI/022_UI_PurifyChamber.rb create mode 100644 Data/Scripts/016_UI/023_UI_MysteryGift.rb create mode 100644 Data/Scripts/016_UI/024_UI_TextEntry.rb create mode 100644 Data/Scripts/017_Minigames/001_Minigame_Duel.rb create mode 100644 Data/Scripts/017_Minigames/002_Minigame_TripleTriad.rb create mode 100644 Data/Scripts/017_Minigames/003_Minigame_SlotMachine.rb create mode 100644 Data/Scripts/017_Minigames/004_Minigame_VoltorbFlip.rb create mode 100644 Data/Scripts/017_Minigames/005_Minigame_Lottery.rb create mode 100644 Data/Scripts/017_Minigames/006_Minigame_Mining.rb create mode 100644 Data/Scripts/017_Minigames/007_Minigame_TilePuzzles.rb create mode 100644 Data/Scripts/018_Alternate battle modes/001_Battle Frontier/001_Challenge_BattleChallenge.rb create mode 100644 Data/Scripts/018_Alternate battle modes/001_Battle Frontier/002_Challenge_Data.rb create mode 100644 Data/Scripts/018_Alternate battle modes/001_Battle Frontier/003_Challenge_ChooseFoes.rb create mode 100644 Data/Scripts/018_Alternate battle modes/001_Battle Frontier/004_Challenge_Battles.rb create mode 100644 Data/Scripts/018_Alternate battle modes/001_Battle Frontier/005_UI_BattleSwap.rb create mode 100644 Data/Scripts/018_Alternate battle modes/001_SafariZone.rb create mode 100644 Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/001_Challenge_ChallengeRules.rb create mode 100644 Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/002_Challenge_Rulesets.rb create mode 100644 Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/003_Challenge_EntryRestrictions.rb create mode 100644 Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/004_Challenge_LevelAdjustment.rb create mode 100644 Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/005_Challenge_BattleRules.rb create mode 100644 Data/Scripts/018_Alternate battle modes/002_BugContest.rb create mode 100644 Data/Scripts/018_Alternate battle modes/003_Battle Frontier generator/001_ChallengeGenerator_Data.rb create mode 100644 Data/Scripts/018_Alternate battle modes/003_Battle Frontier generator/002_ChallengeGenerator_Pokemon.rb create mode 100644 Data/Scripts/018_Alternate battle modes/003_Battle Frontier generator/003_ChallengeGenerator_Trainers.rb create mode 100644 Data/Scripts/018_Alternate battle modes/003_Battle Frontier generator/004_ChallengeGenerator_BattleSim.rb create mode 100644 Data/Scripts/019_Utilities/001_Utilities.rb create mode 100644 Data/Scripts/019_Utilities/002_Utilities_Pokemon.rb create mode 100644 Data/Scripts/019_Utilities/003_Utilities_BattleAudio.rb create mode 100644 Data/Scripts/020_Debug/001_Editor screens/001_EditorScreens.rb create mode 100644 Data/Scripts/020_Debug/001_Editor screens/002_EditorScreens_TerrainTags.rb create mode 100644 Data/Scripts/020_Debug/001_Editor screens/003_EditorScreens_MapConnections.rb create mode 100644 Data/Scripts/020_Debug/001_Editor screens/004_EditorScreens_SpritePositioning.rb create mode 100644 Data/Scripts/020_Debug/001_Editor_Utilities.rb create mode 100644 Data/Scripts/020_Debug/002_Animation editor/001_AnimEditor_SceneElements.rb create mode 100644 Data/Scripts/020_Debug/002_Animation editor/002_AnimEditor_ControlsButtons.rb create mode 100644 Data/Scripts/020_Debug/002_Animation editor/003_AnimEditor_Interpolation.rb create mode 100644 Data/Scripts/020_Debug/002_Animation editor/004_AnimEditor_ExportImport.rb create mode 100644 Data/Scripts/020_Debug/002_Animation editor/005_AnimEditor_Functions.rb create mode 100644 Data/Scripts/020_Debug/002_Editor_DataTypes.rb create mode 100644 Data/Scripts/020_Debug/003_Debug menus/001_Debug_Menus.rb create mode 100644 Data/Scripts/020_Debug/003_Debug menus/002_Debug_MenuCommands.rb create mode 100644 Data/Scripts/020_Debug/003_Debug menus/003_Debug_MenuExtraCode.rb create mode 100644 Data/Scripts/020_Debug/003_Debug menus/004_Debug_MenuSpriteRenamer.rb create mode 100644 Data/Scripts/020_Debug/003_Debug menus/005_Debug_PokemonCommands.rb create mode 100644 Data/Scripts/020_Debug/003_Editor_Listers.rb create mode 100644 Data/Scripts/021_Compiler/001_Compiler.rb create mode 100644 Data/Scripts/021_Compiler/002_Compiler_CompilePBS.rb create mode 100644 Data/Scripts/021_Compiler/003_Compiler_WritePBS.rb create mode 100644 Data/Scripts/021_Compiler/004_Compiler_MapsAndEvents.rb create mode 100644 Data/Scripts/025-Randomizer/Random Pokemon.rb create mode 100644 Data/Scripts/025-Randomizer/RandomizerSettings.rb create mode 100644 Data/Scripts/025-Randomizer/RandomizerUtils.rb create mode 100644 Data/Scripts/025-Randomizer/randomizer - encounters.rb create mode 100644 Data/Scripts/025-Randomizer/randomizer gym leader edit.rb create mode 100644 Data/Scripts/025-Randomizer/randomizer.rb create mode 100644 Data/Scripts/048_Fusion/DoublePreviewScreen.rb create mode 100644 Data/Scripts/048_Fusion/FusedSpecies.rb create mode 100644 Data/Scripts/048_Fusion/FusionAnim.rb create mode 100644 Data/Scripts/048_Fusion/FusionMenu.rb create mode 100644 Data/Scripts/048_Fusion/FusionMovesMenu.rb create mode 100644 Data/Scripts/048_Fusion/FusionPreviewScreen.rb create mode 100644 Data/Scripts/048_Fusion/PokemonFusion.rb create mode 100644 Data/Scripts/048_Fusion/SplitNames.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/001_PifSpriteBitmapCache.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/002_PIFSpriteExtracter.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/AutogenExtracter.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/BaseSpriteExtracter.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/BattleSpriteLoader.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/CustomSpriteExtracter.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/PIFSprite.rb create mode 100644 Data/Scripts/048_Fusion/Sprites/SpritesSubstitutions.rb create mode 100644 Data/Scripts/049_Compatibility/Constants.rb create mode 100644 Data/Scripts/049_Compatibility/DeprecatedClasses.rb create mode 100644 Data/Scripts/049_Compatibility/EggGroups.rb create mode 100644 Data/Scripts/049_Compatibility/MarinUtilities.rb create mode 100644 Data/Scripts/049_Compatibility/PBItems.rb create mode 100644 Data/Scripts/049_Compatibility/PBMoves.rb create mode 100644 Data/Scripts/049_Compatibility/PBSpecies.rb create mode 100644 Data/Scripts/049_Compatibility/PBTrainers.rb create mode 100644 Data/Scripts/049_Compatibility/UtilityMethods.rb create mode 100644 Data/Scripts/049_Compatibility/pb_types.rb create mode 100644 Data/Scripts/050_Outfits/001_OutfitsMain/LayeredClothes.rb create mode 100644 Data/Scripts/050_Outfits/001_OutfitsMain/OutfitSelector.rb create mode 100644 Data/Scripts/050_Outfits/001_OutfitsMain/OutfitsGlobal.rb create mode 100644 Data/Scripts/050_Outfits/001_OutfitsMain/OutfitsSearch.rb create mode 100644 Data/Scripts/050_Outfits/ItemSets.rb create mode 100644 Data/Scripts/050_Outfits/OutfitIds.rb create mode 100644 Data/Scripts/050_Outfits/UI/CharacterSelectMenu.rb create mode 100644 Data/Scripts/050_Outfits/UI/CharacterSelectMenuPresenter.rb create mode 100644 Data/Scripts/050_Outfits/UI/LayeredClothes_Menus.rb create mode 100644 Data/Scripts/050_Outfits/UI/PokemonHatScreenPresenter.rb create mode 100644 Data/Scripts/050_Outfits/UI/PokemonHatScreenView.rb create mode 100644 Data/Scripts/050_Outfits/UI/TrainerClothesPreview.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/0_OutfitsMartAdapter.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/ClothesMartAdapter.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/ClothesShop.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/ClothesShopPresenter.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/ClothesShopPresenter_HatsMenu.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/ClothesShopView.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/HairMartAdapter.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/HairShopPresenter.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/HatShopView.rb create mode 100644 Data/Scripts/050_Outfits/UI/clothesShop/HatsMartAdapter.rb create mode 100644 Data/Scripts/050_Outfits/UI/hairMenu/HairStyleSelectionMenuView.rb create mode 100644 Data/Scripts/050_Outfits/UI/hairMenu/HairstyleSelectionMenuPresenter.rb create mode 100644 Data/Scripts/050_Outfits/utils/OutfitFilenameUtils.rb create mode 100644 Data/Scripts/050_Outfits/utils/OutfitsGameplayUtils.rb create mode 100644 Data/Scripts/050_Outfits/wrappers/001_Outfit.rb create mode 100644 Data/Scripts/050_Outfits/wrappers/Clothes.rb create mode 100644 Data/Scripts/050_Outfits/wrappers/Hairstyle.rb create mode 100644 Data/Scripts/050_Outfits/wrappers/Hat.rb create mode 100644 Data/Scripts/051_Wrappers/quest_reward.rb create mode 100644 Data/Scripts/051_Wrappers/type_expert.rb create mode 100644 Data/Scripts/052_InfiniteFusion/AttributeReader.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Autosave.rb create mode 100644 Data/Scripts/052_InfiniteFusion/BattleLounge.rb create mode 100644 Data/Scripts/052_InfiniteFusion/BetterRegionMap.rb create mode 100644 Data/Scripts/052_InfiniteFusion/CustomTrainers.rb create mode 100644 Data/Scripts/052_InfiniteFusion/DevUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/DisplayText.rb create mode 100644 Data/Scripts/052_InfiniteFusion/DoubleAbilities.rb create mode 100644 Data/Scripts/052_InfiniteFusion/DoubleAbilitiesHandlersOverrides.rb create mode 100644 Data/Scripts/052_InfiniteFusion/DoubleAbilities_UI.rb create mode 100644 Data/Scripts/052_InfiniteFusion/EggMoveTutor.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ExperimentalOptions.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ExportScripts.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Footprints.rb create mode 100644 Data/Scripts/052_InfiniteFusion/FusionMoveTutor.rb create mode 100644 Data/Scripts/052_InfiniteFusion/FusionSprites.rb create mode 100644 Data/Scripts/052_InfiniteFusion/FusionUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/GameOptions.rb create mode 100644 Data/Scripts/052_InfiniteFusion/GameplayUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Gen 2.rb create mode 100644 Data/Scripts/052_InfiniteFusion/GeneralUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/GuessPokemonQuiz.rb create mode 100644 Data/Scripts/052_InfiniteFusion/HiddenAbilityMaps.rb create mode 100644 Data/Scripts/052_InfiniteFusion/HttpCalls.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/000_Shinies_Credits.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/SHINY_COLOR_OFFSETS.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_AltSprites.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_AnimatedBitmap.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_Bitmap.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_Pokedex.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_PokemonIcons.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_PokemonStorage.rb create mode 100644 Data/Scripts/052_InfiniteFusion/ImprovedShinies/Shinies_Species.rb create mode 100644 Data/Scripts/052_InfiniteFusion/IntroScreen.rb create mode 100644 Data/Scripts/052_InfiniteFusion/MapExporter.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Movie.rb create mode 100644 Data/Scripts/052_InfiniteFusion/MultiSaves.rb create mode 100644 Data/Scripts/052_InfiniteFusion/New Balls.rb create mode 100644 Data/Scripts/052_InfiniteFusion/New HMs.rb create mode 100644 Data/Scripts/052_InfiniteFusion/New Items effects.rb create mode 100644 Data/Scripts/052_InfiniteFusion/NonMoneyShop.rb create mode 100644 Data/Scripts/052_InfiniteFusion/OnlineWondertrade.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Overrides.rb create mode 100644 Data/Scripts/052_InfiniteFusion/OverworldShadows.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Pathfinding.rb create mode 100644 Data/Scripts/052_InfiniteFusion/PokedexUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/PokemonSelection.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Quests/PoliceQuest.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Quests/QuestIcons.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Quests/QuestLogScript.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Quests/Quests.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Quests/TRQuests.rb create mode 100644 Data/Scripts/052_InfiniteFusion/RandomAddOns.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Silhouette.rb create mode 100644 Data/Scripts/052_InfiniteFusion/SpeechBubbles.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Spped Up.rb create mode 100644 Data/Scripts/052_InfiniteFusion/SpriteCreditsUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/TeamFlags.rb create mode 100644 Data/Scripts/052_InfiniteFusion/TempEvents.rb create mode 100644 Data/Scripts/052_InfiniteFusion/TrainerCardBackgrounds.rb create mode 100644 Data/Scripts/052_InfiniteFusion/TrainerGeneratorUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Trainers Rebattle.rb create mode 100644 Data/Scripts/052_InfiniteFusion/TripleFusion.rb create mode 100644 Data/Scripts/052_InfiniteFusion/UI_Pokedex_SpritesPage.rb create mode 100644 Data/Scripts/052_InfiniteFusion/UnrealTime.rb create mode 100644 Data/Scripts/052_InfiniteFusion/WaterEffect.rb create mode 100644 Data/Scripts/052_InfiniteFusion/WonderTrade_names.rb create mode 100644 Data/Scripts/052_InfiniteFusion/Wondertrade.rb create mode 100644 Data/Scripts/052_InfiniteFusion/dynamic_waterfall.rb create mode 100644 Data/Scripts/052_InfiniteFusion/k_scriptsUtils.rb create mode 100644 Data/Scripts/052_InfiniteFusion/mapExporter2.rb create mode 100644 Data/Scripts/052_InfiniteFusion/mapExporter2_ui.rb create mode 100644 Data/Scripts/052_InfiniteFusion/platform.rb create mode 100644 Data/Scripts/052_Tests/FusionUtilsTests.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DiveGraphics.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_Encounters.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_EncountersMethods.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_MapLayout.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_Overworld.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_TownMap.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_Utils.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/DynamicWeather/DynamicWeather_Weather.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/HoennGameplayUtils.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/Hoenn_Rival.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/OverworldPokemon.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/PokemartMapTransfers.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/SurfingSplashes.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/01_battled_trainer_random_event.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/02_battled_trainer.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/03_TrainerRematch_System.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/04_TrainerRematch_Battle.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/StartersSelectionScene.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/TrainerRematch_FAVORITE_TYPES.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/TrainerRematch_FRIENDSHIP.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/TrainerRematch_Menus.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/TrainerRematch_Partner.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/TrainerRematch_RandomEvents.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/TrainerRematches/TrainerRematch_Trade.rb create mode 100644 Data/Scripts/053_PIF_Hoenn/WeatherSystem/Configuration.rb create mode 100644 Data/Scripts/999_Main/999_Main.rb create mode 100644 Data/Scripts/DownloadedSettings.rb diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c228e9b5b..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "Data/Scripts"] - path = Data/Scripts - url = https://github.com/infinitefusion/scripts.git diff --git a/Data/.DS_Store b/Data/.DS_Store index e44427339e00e4aaea60a7b20ddbb8ad4d206ec2..a800b5a7aa2b65814fabdfdd0a028f57e3fadc9d 100644 GIT binary patch delta 73 zcmV-P0Ji^x-~)u<1F!_z0lJe~(s3yodwVo8Ff1S~H#IYTEFgO^F)=nQAU82JeSHZC f6(cV@QeSIqZEkOjlkw0W0gbc0+Gqo_C*Iu=t)3c; delta 26 icmZp9z})hHc>~L>$x9vgZ9Z}= 6) ? 16 : 8 + + # The odds of a wild Pokémon/bred egg having Pokérus (out of 65536). + POKERUS_CHANCE = 3 + # Whether a bred baby Pokémon can inherit any TM/HM moves from its father. It + # can never inherit TM/HM moves from its mother. + BREEDING_CAN_INHERIT_MACHINE_MOVES = (MECHANICS_GENERATION <= 5) + # Whether a bred baby Pokémon can inherit egg moves from its mother. It can + # always inherit egg moves from its father. + BREEDING_CAN_INHERIT_EGG_MOVES_FROM_MOTHER = (MECHANICS_GENERATION >= 6) + + KANTO_STARTERS = [:BULBASAUR, :CHARMANDER, :SQUIRTLE] + JOHTO_STARTERS = [:CHIKORITA, :CYNDAQUIL, :TOTODILE] + HOENN_STARTERS = [:TREECKO, :TORCHIC, :MUDKIP] + SINNOH_STARTERS = [:TURTWIG, :CHIMCHAR, :PIPLUP] + KALOS_STARTERS = [:CHESPIN, :FENNEKIN, :FROAKIE] + + + #============================================================================= + + # The amount of money the player starts the game with. + INITIAL_MONEY = 3000 + # The maximum amount of money the player can have. + MAX_MONEY = 999_999 + # The maximum number of Game Corner coins the player can have. + MAX_COINS = 99_999 + # The maximum number of Battle Points the player can have. + MAX_BATTLE_POINTS = 9_999 + # The maximum amount of soot the player can have. + MAX_SOOT = 9_999 + # The maximum length, in characters, that the player's name can be. + MAX_PLAYER_NAME_SIZE = 10 + # The maximum number of Pokémon that can be in the party. + MAX_PARTY_SIZE = 6 + + #============================================================================= + + # A set of arrays each containing a trainer type followed by a Global Variable + # number. If the variable isn't set to 0, then all trainers with the + # associated trainer type will be named as whatever is in that variable. + RIVAL_NAMES = [ + [:RIVAL1, 12], + [:RIVAL2, 12], + [:CHAMPION, 12] + ] + + #============================================================================= + + # Whether outdoor maps should be shaded according to the time of day. + TIME_SHADING = true + + #============================================================================= + + # Whether poisoned Pokémon will lose HP while walking around in the field. + POISON_IN_FIELD = true #(MECHANICS_GENERATION <= 4) + # Whether poisoned Pokémon will faint while walking around in the field + # (true), or survive the poisoning with 1 HP (false). + POISON_FAINT_IN_FIELD = false + # Whether planted berries grow according to Gen 4 mechanics (true) or Gen 3 + # mechanics (false). + NEW_BERRY_PLANTS = true + # Whether fishing automatically hooks the Pokémon (true), or whether there is + # a reaction test first (false). + FISHING_AUTO_HOOK = false + # The ID of the common event that runs when the player starts fishing (runs + # instead of showing the casting animation). + FISHING_BEGIN_COMMON_EVENT = -1 + # The ID of the common event that runs when the player stops fishing (runs + # instead of showing the reeling in animation). + FISHING_END_COMMON_EVENT = -1 + + #============================================================================= + + # The number of steps allowed before a Safari Zone game is over (0=infinite). + SAFARI_STEPS = 600 + # The number of seconds a Bug Catching Contest lasts for (0=infinite). + BUG_CONTEST_TIME = 20 * 60 # 20 minutes + + #============================================================================= + + # Pairs of map IDs, where the location signpost isn't shown when moving from + # one of the maps in a pair to the other (and vice versa). Useful for single + # long routes/towns that are spread over multiple maps. + # e.g. [4,5,16,17,42,43] will be map pairs 4,5 and 16,17 and 42,43. + # Moving between two maps that have the exact same name won't show the + # location signpost anyway, so you don't need to list those maps here. + NO_SIGNPOSTS = [] + + #============================================================================= + + # Whether you need at least a certain number of badges to use some hidden + # moves in the field (true), or whether you need one specific badge to use + # them (false). The amounts/specific badges are defined below. + FIELD_MOVES_COUNT_BADGES = true + # Depending on FIELD_MOVES_COUNT_BADGES, either the number of badges required + # to use each hidden move in the field, or the specific badge number required + # to use each move. Remember that badge 0 is the first badge, badge 1 is the + # second badge, etc. + # e.g. To require the second badge, put false and 1. + # To require at least 2 badges, put true and 2. + BADGE_FOR_CUT = 1 + BADGE_FOR_FLASH = 1 + BADGE_FOR_ROCKSMASH = 0 + BADGE_FOR_SURF = 6 + BADGE_FOR_FLY = 3 + BADGE_FOR_STRENGTH = 5 + BADGE_FOR_DIVE = 9 + BADGE_FOR_WATERFALL = 9 + BADGE_FOR_TELEPORT = 3 + BADGE_FOR_BOUNCE = 8 + BADGE_FOR_ROCKCLIMB = 16 + #============================================================================= + + # If a move taught by a TM/HM/TR replaces another move, this setting is + # whether the machine's move retains the replaced move's PP (true), or whether + # the machine's move has full PP (false). + TAUGHT_MACHINES_KEEP_OLD_PP = (MECHANICS_GENERATION == 5) + # Whether the Black/White Flutes will raise/lower the levels of wild Pokémon + # respectively (true), or will lower/raise the wild encounter rate + # respectively (false). + FLUTES_CHANGE_WILD_ENCOUNTER_LEVELS = (MECHANICS_GENERATION >= 6) + # Whether Repel uses the level of the first Pokémon in the party regardless of + # its HP (true), or it uses the level of the first unfainted Pokémon (false). + REPEL_COUNTS_FAINTED_POKEMON = (MECHANICS_GENERATION >= 6) + # Whether Rage Candy Bar acts as a Full Heal (true) or a Potion (false). + RAGE_CANDY_BAR_CURES_STATUS_PROBLEMS = (MECHANICS_GENERATION >= 7) + + #============================================================================= + + # The name of the person who created the Pokémon storage system. + def self.storage_creator_name + return _INTL("Bill") + end + + # The number of boxes in Pokémon storage. + NUM_STORAGE_BOXES = 40 + + #============================================================================= + + # The names of each pocket of the Bag. Ignore the first entry (""). + def self.bag_pocket_names + return ["", + _INTL("Items"), + _INTL("Medicine"), + _INTL("Poké Balls"), + _INTL("TMs & HMs"), + _INTL("Berries"), + _INTL("Mail"), + _INTL("Battle Items"), + _INTL("Key Items") + ] + end + + # The maximum number of slots per pocket (-1 means infinite number). Ignore + # the first number (0). + BAG_MAX_POCKET_SIZE = [0, -1, -1, -1, -1, -1, -1, -1, -1] + # The maximum number of items each slot in the Bag can hold. + BAG_MAX_PER_SLOT = 999 + # Whether each pocket in turn auto-sorts itself by item ID number. Ignore the + # first entry (the 0). + BAG_POCKET_AUTO_SORT = [0, false, false, false, true, true, false, false, false] + + #============================================================================= + + # Whether the Pokédex list shown is the one for the player's current region + # (true), or whether a menu pops up for the player to manually choose which + # Dex list to view if more than one is available (false). + USE_CURRENT_REGION_DEX = false + # The names of the Pokédex lists, in the order they are defined in the PBS + # file "regionaldexes.txt". The last name is for the National Dex and is added + # onto the end of this array (remember that you don't need to use it). This + # array's order is also the order of $Trainer.pokedex.unlocked_dexes, which + # records which Dexes have been unlocked (the first is unlocked by default). + # If an entry is just a name, then the region map shown in the Area page while + # viewing that Dex list will be the region map of the region the player is + # currently in. The National Dex entry should always behave like this. + # If an entry is of the form [name, number], then the number is a region + # number. That region's map will appear in the Area page while viewing that + # Dex list, no matter which region the player is currently in. + def self.pokedex_names + return [ + # [_INTL("Kanto Pokédex"), 0] + ] + end + + # Whether all forms of a given species will be immediately available to view + # in the Pokédex so long as that species has been seen at all (true), or + # whether each form needs to be seen specifically before that form appears in + # the Pokédex (false). + DEX_SHOWS_ALL_FORMS = false + # An array of numbers, where each number is that of a Dex list (in the same + # order as above, except the National Dex is -1). All Dex lists included here + # will begin their numbering at 0 rather than 1 (e.g. Victini in Unova's Dex). + DEXES_WITH_OFFSETS = [] + + + #============================================================================= + + # A set of arrays, each containing details of a graphic to be shown on the + # region map if appropriate. The values for each array are as follows: + # * Region number. + # * Game Switch; the graphic is shown if this is ON (non-wall maps only). + # * X coordinate of the graphic on the map, in squares. + # * Y coordinate of the graphic on the map, in squares. + # * Name of the graphic, found in the Graphics/Pictures folder. + # * The graphic will always (true) or never (false) be shown on a wall map. + REGION_MAP_EXTRAS = [ + #[0, 51, 16, 15, "mapHiddenBerth", false], + #[0, 52, 20, 14, "mapHiddenFaraday", false] + ] + + TRIPLE_TYPES = [:QMARKS,:ICEFIREELECTRIC,:FIREWATERELECTRIC,:WATERGROUNDFLYING,:GHOSTSTEELWATER, + :FIREWATERGRASS,:GRASSSTEEL,:BUGSTEELPSYCHIC,:ICEROCKSTEEL] + + #============================================================================= + + # A list of maps used by roaming Pokémon. Each map has an array of other maps + # it can lead to. + ROAMING_AREAS = { + 262 => [261,311], + 311 => [262,312], + 312 => [311], + 261 => [262,288,267], + 288 => [261,267,285], + 267 => [261,288,300,254], + 284 => [288,266,285], + 300 => [267,254], + 254 => [300,265], + 266 => [284,265], + 265 => [266,254], + 285 => [284,288]} + + SEVII_ROAMING = { + 528 => [526], #Treasure beach + 526 => [528,559], #Knot Island + 559 => [526,561,564], #Kindle Road + 561 => [559], #Mt. Ember + 564 => [559,562,563,594], #brine road + 562 => [564], #boon island + 563 => [564,600] , #kin island + 594 => [564,566,603], #water labyrinth + 600 => [563,619], #bond bridge + 619 => [600] , #Berry forest + 566 => [594,603], #Resort gorgeous + 603 => [566,594], #Chrono Island + } + # A set of arrays, each containing the details of a roaming Pokémon. The + # information within each array is as follows: + # * Species. + # * Level. + # * Game Switch; the Pokémon roams while this is ON. + # * Encounter type (0=any, 1=grass/walking in cave, 2=surfing, 3=fishing, + # 4=surfing/fishing). See the bottom of PField_RoamingPokemon for lists. + # * Name of BGM to play for that encounter (optional). + # * Roaming areas specifically for this Pokémon (optional). + ROAMING_SPECIES = [ + [:ENTEI, 50, 350, 1, "Legendary Birds",ROAMING_AREAS,:Sunny], + [:B245H243, 50, 341, 1, "Legendary Birds",ROAMING_AREAS,:Storm], + [:B379H378, 50, 602, 0, "Legendary Birds",SEVII_ROAMING,:StrongWinds], + [:B378H379, 50, 602, 0, "Legendary Birds",SEVII_ROAMING,:StrongWinds], + [:FEEBAS, 15, 4, 3, "Pokemon HeartGold and SoulSilver - Wild Pokemon Battle (Kanto)",SEVII_ROAMING,:Rain] + ] + + PINKAN_ISLAND_MAPS=[51,46,428,531] + + #============================================================================= + + # A set of arrays, each containing the details of a wild encounter that can + # only occur via using the Poké Radar. The information within each array is as + # follows: + # * Map ID on which this encounter can occur. + # * Probability that this encounter will occur (as a percentage). + # * Species. + # * Minimum possible level. + # * Maximum possible level (optional). + POKE_RADAR_ENCOUNTERS = [ + [78, 50, :FLETCHLING,2,5], #Rt. 1 + [86, 50, :FLETCHLING,2,5], #Rt. 2 + [90, 50, :FLETCHLING,2,5], #Rt. 2 + [491, 50, :SHROOMISH,2,5], #Viridian Forest + [490, 50, :BUDEW,4,9], #Rt. 3 + [106, 50, :NINCADA,8,10], #Rt. 4 + [12, 50, :TOGEPI,10,10], #Rt. 5 + [16, 50, :SLAKOTH,12,15], #Rt. 6 + [413, 50, :DRIFLOON,17,20], #Rt. 7 + [409, 50, :SHINX,17,18], #Rt. 8 + [495, 50, :ARON,12,15], #Rt. 9 + [351, 50, :ARON,12,15], #Rt. 9 + [154, 50, :KLINK,14,17], #Rt. 10 + [155, 50, :NINCADA,12,15], #Rt. 11 + [159, 50, :COTTONEE,22,25], #Rt. 12 + [437, 50, :COTTONEE,22,25], #Rt. 13 + [437, 50, :JOLTIK,22,25], #Rt. 13 + [440, 50, :JOLTIK,22,25], #Rt. 14 + [444, 50, :SOLOSIS,22,25], #Rt. 15 + [438, 50, :NATU,22,25], #Rt. 16 + [146, 50, :KLEFKI,22,25], #Rt. 17 + [517, 50, :FERROSEED,22,25], #Rt. 18 + [445, 50, :BAGON,20,20], #Safari zone 1 + [484, 50, :AXEW,20,20], #Safari zone 2 + [485, 50, :DEINO,20,20], #Safari zone 3 + [486, 50, :LARVITAR,20,20], #Safari zone 4 + [487, 50, :JANGMOO,20,20], #Safari zone 5 + [59, 50, :DUNSPARCE,25,30], #Rt. 21 + [171, 50, :BIDOOF,2,5], #Rt. 22 + [143, 50, :RIOLU,25,25], #Rt. 23 + [8, 50, :BUNEARY,12,13], #Rt. 24 + [145, 50, :ABSOL,30,35], #Rt. 26 + [147, 50, :ABSOL,30,35], #Rt. 27 + [311, 50, :BIDOOF,5,5], #Rt. 29 + [284, 50, :LUXIO,40,45], #Rt. 33 + [288, 50, :VIGOROTH,40,45], #Rt. 32 + [342, 50, :GOLETT,40,45], #Ruins of Alph + [261, 50, :BELLOSSOM,45,50], #Rt. 31 + [262, 50, :BIBAREL,45,50], #Rt. 30 + [265, 50, :KIRLIA,25,30], #Rt. 34 + [254, 50, :SMEARGLE,25,30], #Rt. 35 + [267, 50, :SUDOWOODO,25,30], #Rt. 36 + [500, 50, :FOMANTIS,30,30], #National Park + [266, 50, :BRELOOM,30,30], #Ilex Forest + [670, 50, :WEAVILE,50,50], #Ice mountains + [528, 50, :PYUKUMUKU,20,20], #Treasure Beach + [690, 50, :OCTILLERY,32,45], #Deep Ocean + [561, 50, :FLETCHINDER,32,45], #Mt. Ember + [562, 50, :NINJASK,45,50], #Boon Island + [603, 50, :KECLEON,45,50], #Chrono Island + [654, 50, :WHIMSICOTT,32,45], #Brine Road + [559, 50, :SCRAGGY,32,45] #Kindle Road + ] + + #============================================================================= + + # The Game Switch that is set to ON when the player blacks out. + STARTING_OVER_SWITCH = 1 + # The Game Switch that is set to ON when the player has seen Pokérus in the + # Poké Center (and doesn't need to be told about it again). + SEEN_POKERUS_SWITCH = 2 + # The Game Switch which, while ON, makes all wild Pokémon created be shiny. + SHINY_WILD_POKEMON_SWITCH = 31 + # The Game Switch which, while ON, makes all Pokémon created considered to be + # met via a fateful encounter. + FATEFUL_ENCOUNTER_SWITCH = 32 + + #============================================================================= + + # ID of the animation played when the player steps on grass (grass rustling). + GRASS_ANIMATION_ID = 1 + # ID of the animation played when the player lands on the ground after hopping + # over a ledge (shows a dust impact). + DUST_ANIMATION_ID = 2 + # ID of the animation played when a trainer notices the player (an exclamation + # bubble). + EXCLAMATION_ANIMATION_ID = 3 + # ID of the animation played when a patch of grass rustles due to using the + # Poké Radar. + RUSTLE_NORMAL_ANIMATION_ID = 1 + # ID of the animation played when a patch of grass rustles vigorously due to + # using the Poké Radar. (Rarer species) + RUSTLE_VIGOROUS_ANIMATION_ID = 5 + # ID of the animation played when a patch of grass rustles and shines due to + # using the Poké Radar. (Shiny encounter) + RUSTLE_SHINY_ANIMATION_ID = 6 + # ID of the animation played when a berry tree grows a stage while the player + # is on the map (for new plant growth mechanics only). + PLANT_SPARKLE_ANIMATION_ID = 7 + SPARKLE_SHORT_ANIMATION_ID = 25 + SPARKLE_SUBTLE_ANIMATION_ID = 29 + + SLEEP_ANIMATION_ID = 26 + + CUT_TREE_ANIMATION_ID = 19 + ROCK_SMASH_ANIMATION_ID = 20 + + #============================================================================= + + # An array of available languages in the game, and their corresponding message + # file in the Data folder. Edit only if you have 2 or more languages to choose + # from. + LANGUAGES = [ + # ["English", "english.dat"], + # ["Deutsch", "deutsch.dat"] + ] + + + #Technical + SPRITE_CACHE_MAX_NB=100 + NEWEST_SPRITEPACK_MONTH = 12 + NEWEST_SPRITEPACK_YEAR = 2020 + #============================================================================= + + # Available speech frames. These are graphic files in "Graphics/Windowskins/". + SPEECH_WINDOWSKINS = [ + "speech hgss 1", + "speech hgss 2", + "speech hgss 3", + "speech hgss 4", + "speech hgss 5", + "speech hgss 6", + "speech hgss 7", + "speech hgss 8", + "speech hgss 9", + "speech hgss 10", + "speech hgss 11", + "speech hgss 12", + "speech hgss 13", + "speech hgss 14", + "speech hgss 15", + "speech hgss 16", + "speech hgss 17", + "speech hgss 18", + "speech hgss 19", + "speech hgss 20", + "speech pl 18" + ] + + # Available menu frames. These are graphic files in "Graphics/Windowskins/". + MENU_WINDOWSKINS = [ + "default_transparent", + "default_opaque", + "choice 2", + "choice 3", + "choice 4", + "choice 5", + "choice 6", + "choice 7", + "choice 8", + "choice 9", + "choice 10", + "choice 11", + "choice 12", + "choice 13", + "choice 14", + "choice 15", + "choice 16", + "choice 17", + "choice 18", + "choice 19", + "choice 20", + "choice 21", + "choice 22", + "choice 23", + "choice 24", + "choice 25", + "choice 26", + "choice 27", + "choice 28" + ] + + + RANDOMIZED_GYM_TYPE_TM= + { + :NORMAL => [:TM32,:TM49,:TM42,:TM98], #DOUBLETEAM ECHOEDVOICE FACADE BATONPASS + :FIGHTING => [:TM83,:TM115,:TM52,:TM112], #WORKUP POWERUPPUNCH FOCUSBLAST FOCUSPUNCH + :FLYING => [:TM62,:TM58,:TM108,:TM100], #ACROBATICS SKYDROP SKYATTACK DEFOG + :POISON => [:TM84,:TM06,:TM36,:TM34], #POISONJAB TOXIC SLUDGEBOMB SLUDGEWAVE + :GROUND => [:TM28,:TM78,:TM26,:TM119], #DIG BULLDOZE EARTHQUAKE STOMPINGTANTRUM + :ROCK => [:TM39,:TM80,:TM71,:TM69], #ROCKTOMB ROCKTHROW STONEDGE ROCKPOLISH + :BUG => [:TM76,:TM89,:TM113,:TM99], #STRUGGLEBUG UTURN INFESTATION QUIVERDANCE + :GHOST => [:TM85,:TM65,:TM30,:TM97], #DREAMEATER SHADOWCLAW SHADOWBALL NASTYPLOT + :STEEL => [:TM74,:TM118,:TM117,:TM75], # GYROBALL STEELWING SMARTSTRIKE SWORDDANCE + :FIRE => [:TM11,:TM43,:TM38,:TM61], #SUNNYDAY FLAMECHARGE FIREBLAST WILLOWISP + :WATER => [:TM55,:TM105,:TM121,:TM18], #WATERPULSE AQUAJET SCALD RAINDANCE + :GRASS => [:TM22,:TM53,:TM86,:TM102], # SOLARBEAM ENERGYBALL GRASSKNOT SPORE + :ELECTRIC => [:TM73,:TM116,:TM93,:TM72], #THUNDERWAVE SHOCKWAVE WILDCHARGE VOLTSWITCH + :PSYCHIC => [:TM77,:TM03,:TM29,:TM04], #PSYCHUP PSYSHOCK PSYCHIC CALMMIND + :ICE => [:TM110,:TM13,:TM14,:TM07], #AURORAVEIL ICEBEAM BLIZZARD HAIL + :DRAGON => [:TM95,:TM02,:TM82,:TM101], #SNARL DRAGONCLAW DRAGONTAIL DRAGONDANCE + :DARK => [:TM95,:TM46,:TM120,:TM97], #SNARL THIEF THROATCHOP NASTYPLOT + :FAIRY => [:TM45,:TM111,:TM96,:TM104] #ATTRACT DAZZLINGGLEAM MOONBLAST RECOVER + } + + EXCLUDE_FROM_RANDOM_SHOPS=[:RARECANDY] + +end + +# DO NOT EDIT THESE! +module Essentials + VERSION = "19.1.dev" + ERROR_TEXT = "" +end diff --git a/Data/Scripts/001_Technical/001_Debugging/001_PBDebug.rb b/Data/Scripts/001_Technical/001_Debugging/001_PBDebug.rb new file mode 100644 index 000000000..dc7e7e91d --- /dev/null +++ b/Data/Scripts/001_Technical/001_Debugging/001_PBDebug.rb @@ -0,0 +1,40 @@ +module PBDebug + @@log = [] + + def self.logonerr + begin + yield + rescue + PBDebug.log("") + PBDebug.log("**Exception: #{$!.message}") + PBDebug.log("#{$!.backtrace.inspect}") + PBDebug.log("") +# if $INTERNAL + pbPrintException($!) +# end + PBDebug.flush + end + end + + def self.flush + if $DEBUG && $INTERNAL && @@log.length>0 + File.open("Data/debuglog.txt", "a+b") { |f| f.write("#{@@log}") } + end + @@log.clear + end + + def self.log(msg) + if $DEBUG && $INTERNAL + @@log.push("#{msg}\r\n") +# if @@log.length>1024 + PBDebug.flush +# end + end + end + + def self.dump(msg) + if $DEBUG && $INTERNAL + File.open("Data/dumplog.txt", "a+b") { |f| f.write("#{msg}\r\n") } + end + end +end diff --git a/Data/Scripts/001_Technical/001_Debugging/002_DebugConsole.rb b/Data/Scripts/001_Technical/001_Debugging/002_DebugConsole.rb new file mode 100644 index 000000000..6eb42fccd --- /dev/null +++ b/Data/Scripts/001_Technical/001_Debugging/002_DebugConsole.rb @@ -0,0 +1,54 @@ +# To use the console, use the executable explicitly built +# with the console enabled on Windows. On Linux and macOS, +# just launch the executable directly from a terminal. +module Console + def self.setup_console + return unless $DEBUG + echoln "--------------------------------" + echoln "#{System.game_title} Output Window" + echoln "--------------------------------" + echoln "If you are seeing this window, you are running" + echoln "#{System.game_title} in Debug Mode. This means" + echoln "that you're either playing a Debug Version, or" + echoln "you are playing from within RPG Maker XP." + echoln "" + echoln "Closing this window will close the game. If" + echoln "you want to get rid of this window, run the" + echoln "program from the Shell, or download a Release" + echoln "version." + echoln "" + echoln "--------------------------------" + echoln "Debug Output:" + echoln "--------------------------------" + echoln "" + end + + def self.readInput + return gets.strip + end + + def self.readInput2 + return self.readInput + end + + def self.get_input + echo self.readInput2 + end +end + +module Kernel + def echo(string) + return unless $DEBUG + printf(string.is_a?(String) ? string : string.inspect) + end + + def echoln(string) + caller_info = caller(1..1).first + file, line, method = caller_info.split(":") + echo "#{file}, #{line}:\t" + echo(string) + echo("\r\n") + end +end + +Console.setup_console diff --git a/Data/Scripts/001_Technical/001_Debugging/003_Errors.rb b/Data/Scripts/001_Technical/001_Debugging/003_Errors.rb new file mode 100644 index 000000000..d744b325b --- /dev/null +++ b/Data/Scripts/001_Technical/001_Debugging/003_Errors.rb @@ -0,0 +1,93 @@ +#=============================================================================== +# Exceptions and critical code +#=============================================================================== +class Reset < Exception +end + +def pbGetExceptionMessage(e,_script="") + emessage = e.message.dup + emessage.force_encoding(Encoding::UTF_8) + if e.is_a?(Hangup) + emessage = "The script is taking too long. The game will restart." + elsif e.is_a?(Errno::ENOENT) + filename = emessage.sub("No such file or directory - ", "") + emessage = "File #{filename} not found." + end + emessage.gsub!(/Section(\d+)/) { $RGSS_SCRIPTS[$1.to_i][1] } rescue nil + return emessage +end + +def pbPrintException(e) + emessage = "" + if $EVENTHANGUPMSG && $EVENTHANGUPMSG!="" + emessage = $EVENTHANGUPMSG # Message with map/event ID generated elsewhere + $EVENTHANGUPMSG = nil + else + emessage = pbGetExceptionMessage(e) + end + # begin message formatting + message = "[Infinite Fusion version #{Settings::GAME_VERSION_NUMBER}]\r\n" + if $game_switches + message += "Randomized trainers, " if $game_switches[SWITCH_RANDOM_TRAINERS] + message += "Randomized gym trainers, " if $game_switches[SWITCH_RANDOMIZE_GYMS_SEPARATELY] + message += "Randomized wild Pokemon (global), " if $game_switches[SWITCH_WILD_RANDOM_GLOBAL] + message += "Randomized wild Pokemon (area), " if $game_switches[RandomizerWildPokemonOptionsScene::RANDOM_WILD_AREA] + message += "All fused, " if $game_switches[SWITCH_RANDOM_TRAINERS] + message += "Randomized trainers, " if $game_switches[RandomizerWildPokemonOptionsScene::REGULAR_TO_FUSIONS] + end + message += "#{Essentials::ERROR_TEXT}\r\n" # For third party scripts to add to + message += "Exception: #{e.class}\r\n" + message += "Message: #{emessage}\r\n" + # show last 10/25 lines of backtrace + message += "\r\nBacktrace:\r\n" + btrace = "" + if e.backtrace + maxlength = ($INTERNAL) ? 25 : 10 + e.backtrace[0, maxlength].each { |i| btrace += "#{i}\r\n" } + end + btrace.gsub!(/Section(\d+)/) { $RGSS_SCRIPTS[$1.to_i][1] } rescue nil + message += btrace + # output to log + errorlog = "errorlog.txt" + errorlog = RTP.getSaveFileName("errorlog.txt") if (Object.const_defined?(:RTP) rescue false) + File.open(errorlog, "ab") do |f| + f.write("\r\n=================\r\n\r\n[#{Time.now}]\r\n") + f.write(message) + end + # format/censor the error log directory + errorlogline = errorlog.gsub("/", "\\") + errorlogline.sub!(Dir.pwd + "\\", "") + errorlogline.sub!(pbGetUserName, "USERNAME") + errorlogline = "\r\n" + errorlogline if errorlogline.length > 20 + # output message + print("#{message}\r\nThis exception was logged in #{errorlogline}.\r\nHold Ctrl when closing this message to copy it to the clipboard.") + # Give a ~500ms coyote time to start holding Control + t = System.delta + until (System.delta - t) >= 500000 + Input.update + if Input.press?(Input::CTRL) + Input.clipboard = message + break + end + end +end + +def pbCriticalCode + ret = 0 + begin + yield + ret = 1 + rescue Exception + e = $! + if e.is_a?(Reset) || e.is_a?(SystemExit) + raise + else + pbPrintException(e) + if e.is_a?(Hangup) + ret = 2 + raise Reset.new + end + end + end + return ret +end diff --git a/Data/Scripts/001_Technical/001_Debugging/004_Validation.rb b/Data/Scripts/001_Technical/001_Debugging/004_Validation.rb new file mode 100644 index 000000000..e3c23ccca --- /dev/null +++ b/Data/Scripts/001_Technical/001_Debugging/004_Validation.rb @@ -0,0 +1,31 @@ +# The Kernel module is extended to include the validate method. +module Kernel + private + + # Used to check whether method arguments are of a given class or respond to a method. + # @param value_pairs [Hash{Object => Class, Array, Symbol}] value pairs to validate + # @example Validate a class or method + # validate foo => Integer, baz => :to_s # raises an error if foo is not an Integer or if baz doesn't implement #to_s + # @example Validate a class from an array + # validate foo => [Sprite, Bitmap, Viewport] # raises an error if foo isn't a Sprite, Bitmap or Viewport + # @raise [ArgumentError] if validation fails + def validate(value_pairs) + unless value_pairs.is_a?(Hash) + raise ArgumentError, "Non-hash argument #{value_pairs.inspect} passed into validate." + end + errors = value_pairs.map do |value, condition| + if condition.is_a?(Array) + unless condition.any? { |klass| value.is_a?(klass) } + next "Expected #{value.inspect} to be one of #{condition.inspect}, but got #{value.class.name}." + end + elsif condition.is_a?(Symbol) + next "Expected #{value.inspect} to respond to #{condition}." unless value.respond_to?(condition) + elsif !value.is_a?(condition) + next "Expected #{value.inspect} to be a #{condition.name}, but got #{value.class.name}." + end + end + errors.compact! + return if errors.empty? + raise ArgumentError, "Invalid argument passed to method.\r\n" + errors.join("\r\n") + end +end diff --git a/Data/Scripts/001_Technical/001_Debugging/005_Deprecation.rb b/Data/Scripts/001_Technical/001_Debugging/005_Deprecation.rb new file mode 100644 index 000000000..64660171a --- /dev/null +++ b/Data/Scripts/001_Technical/001_Debugging/005_Deprecation.rb @@ -0,0 +1,53 @@ +# The Deprecation module is used to warn game & plugin creators of deprecated +# methods. +module Deprecation + module_function + + # Sends a warning of a deprecated method into the debug console. + # @param method_name [String] name of the deprecated method + # @param removal_version [String] version the method is removed in + # @param alternative [String] preferred alternative method + def warn_method(method_name, removal_version = nil, alternative = nil) + text = _INTL('WARN: usage of deprecated method "{1}" or its alias.', method_name) + unless removal_version.nil? + text += _INTL("\nThe method is slated to be"\ + " removed in Essentials {1}.", removal_version) + end + unless alternative.nil? + text += _INTL("\nUse \"{1}\" instead.", alternative) + end + echoln text + end +end + +# The Module class is extended to allow easy deprecation of instance and class methods. +class Module + private + + # Creates a deprecated alias for a method. + # Using it sends a warning to the debug console. + # @param name [Symbol] name of the new alias + # @param aliased_method [Symbol] name of the aliased method + # @param removal_in [String] version the alias is removed in + # @param class_method [Boolean] whether the method is a class method + def deprecated_method_alias(name, aliased_method, removal_in: nil, class_method: false) + validate name => Symbol, aliased_method => Symbol, removal_in => [NilClass, String], + class_method => [TrueClass, FalseClass] + + target = class_method ? self.class : self + class_name = self.name + + unless target.method_defined?(aliased_method) + raise ArgumentError, "#{class_name} does not have method #{aliased_method} defined" + end + + delimiter = class_method ? '.' : '#' + + target.define_method(name) do |*args, **kvargs| + alias_name = format('%s%s%s', class_name, delimiter, name) + aliased_method_name = format('%s%s%s', class_name, delimiter, aliased_method) + Deprecation.warn_method(alias_name, removal_in, aliased_method_name) + method(aliased_method).call(*args, **kvargs) + end + end +end diff --git a/Data/Scripts/001_Technical/001_MKXP_Compatibility.rb b/Data/Scripts/001_Technical/001_MKXP_Compatibility.rb new file mode 100644 index 000000000..6a2270f36 --- /dev/null +++ b/Data/Scripts/001_Technical/001_MKXP_Compatibility.rb @@ -0,0 +1,48 @@ +# Using mkxp-z v2.2.0 - https://gitlab.com/mkxp-z/mkxp-z/-/releases/v2.2.0 +$VERBOSE = nil +Font.default_shadow = false if Font.respond_to?(:default_shadow) +Encoding.default_internal = Encoding::UTF_8 +Encoding.default_external = Encoding::UTF_8 +Graphics.frame_rate = 40 + +def pbSetWindowText(string) + System.set_window_title(string || System.game_title) +end + +class Bitmap + attr_accessor :storedPath + + alias mkxp_draw_text draw_text unless method_defined?(:mkxp_draw_text) + + def draw_text(x, y, width, height, text, align = 0) + if x.is_a?(Rect) + x.y -= (@text_offset_y || 0) + # rect, string & alignment + mkxp_draw_text(x, y, width) + else + y -= (@text_offset_y || 0) + height = text_size(text).height + mkxp_draw_text(x, y, width, height, text, align) + end + end +end + +module Graphics + def self.delta_s + return self.delta.to_f / 1_000_000 + end +end + +def pbSetResizeFactor(factor) + if !$ResizeInitialized + Graphics.resize_screen(Settings::SCREEN_WIDTH, Settings::SCREEN_HEIGHT) + $ResizeInitialized = true + end + if factor < 0 || factor == 4 + Graphics.fullscreen = true if !Graphics.fullscreen + else + Graphics.fullscreen = false if Graphics.fullscreen + Graphics.scale = (factor + 1) * 0.5 + Graphics.center + end +end diff --git a/Data/Scripts/001_Technical/002_Files/001_FileTests.rb b/Data/Scripts/001_Technical/002_Files/001_FileTests.rb new file mode 100644 index 000000000..56def11ef --- /dev/null +++ b/Data/Scripts/001_Technical/002_Files/001_FileTests.rb @@ -0,0 +1,492 @@ +#=============================================================================== +# Reads files of certain format from a directory +#=============================================================================== +class Dir + #----------------------------------------------------------------------------- + # Reads all files in a directory + #----------------------------------------------------------------------------- + def self.get(dir, filters = "*", full = true) + files = [] + filters = [filters] if !filters.is_a?(Array) + self.chdir(dir) do + for filter in filters + self.glob(filter){ |f| files.push(full ? (dir + "/" + f) : f) } + end + end + return files.sort + end + #----------------------------------------------------------------------------- + # Generates entire file/folder tree from a certain directory + #----------------------------------------------------------------------------- + def self.all(dir, filters = "*", full = true) + # sets variables for starting + files = [] + subfolders = [] + for file in self.get(dir, filters, full) + # engages in recursion to read the entire file tree + if self.safe?(file) # Is a directory + subfolders += self.all(file, filters, full) + else # Is a file + files += [file] + end + end + # returns all found files + return files + subfolders + end + #----------------------------------------------------------------------------- + # Checks for existing directory, gets around accents + #----------------------------------------------------------------------------- + def self.safe?(dir) + return false if !FileTest.directory?(dir) + ret = false + self.chdir(dir) { ret = true } rescue nil + return ret + end + #----------------------------------------------------------------------------- +end + + + +#=============================================================================== +# extensions for file class +#=============================================================================== +class File + #----------------------------------------------------------------------------- + # Checks for existing file, gets around accents + #----------------------------------------------------------------------------- + def self.safe?(file) + ret = false + self.open(file, 'rb') { ret = true } rescue nil + return ret + end + #----------------------------------------------------------------------------- +end + + + +#=============================================================================== +# Checking for files and directories +#=============================================================================== +# Works around a problem with FileTest.directory if directory contains accent marks +def safeIsDirectory?(f) + ret = false + Dir.chdir(f) { ret = true } rescue nil + return ret +end + +# Works around a problem with FileTest.exist if path contains accent marks +def safeExists?(f) + return FileTest.exist?(f) if f[/\A[\x20-\x7E]*\z/] + ret = false + begin + File.open(f,"rb") { ret = true } + rescue Errno::ENOENT, Errno::EINVAL, Errno::EACCES + ret = false + end + return ret +end + +# Similar to "Dir.glob", but designed to work around a problem with accessing +# files if a path contains accent marks. +# "dir" is the directory path, "wildcard" is the filename pattern to match. +def safeGlob(dir,wildcard) + ret = [] + afterChdir = false + begin + Dir.chdir(dir) { + afterChdir = true + Dir.glob(wildcard) { |f| ret.push(dir+"/"+f) } + } + rescue Errno::ENOENT + raise if afterChdir + end + if block_given? + ret.each { |f| yield(f) } + end + return (block_given?) ? nil : ret +end + +def pbResolveAudioSE(file) + return nil if !file + if RTP.exists?("Audio/SE/"+file,["",".wav",".mp3",".ogg"]) + return RTP.getPath("Audio/SE/"+file,["",".wav",".mp3",".ogg"]) + end + return nil +end + +# Finds the real path for an image file. This includes paths in encrypted +# archives. Returns nil if the path can't be found. +def pbResolveBitmap(x) + return nil if !x + noext = x.gsub(/\.(bmp|png|gif|jpg|jpeg)$/,"") + filename = nil +# RTP.eachPathFor(x) { |path| +# filename = pbTryString(path) if !filename +# filename = pbTryString(path+".gif") if !filename +# } + RTP.eachPathFor(noext) { |path| + filename = pbTryString(path+".png") if !filename + filename = pbTryString(path+".gif") if !filename +# filename = pbTryString(path+".jpg") if !filename +# filename = pbTryString(path+".jpeg") if !filename +# filename = pbTryString(path+".bmp") if !filename + } + return filename +end + +# Finds the real path for an image file. This includes paths in encrypted +# archives. Returns _x_ if the path can't be found. +def pbBitmapName(x) + ret = pbResolveBitmap(x) + return (ret) ? ret : x +end + +def strsplit(str, re) + ret = [] + tstr = str + while re =~ tstr + ret[ret.length] = $~.pre_match + tstr = $~.post_match + end + ret[ret.length] = tstr if ret.length + return ret +end + +def canonicalize(c) + csplit = strsplit(c, /[\/\\]/) + pos = -1 + ret = [] + retstr = "" + for x in csplit + if x == ".." + if pos >= 0 + ret.delete_at(pos) + pos -= 1 + end + elsif x != "." + ret.push(x) + pos += 1 + end + end + for i in 0...ret.length + retstr += "/" if i > 0 + retstr += ret[i] + end + return retstr +end + + + +module RTP + @rtpPaths = nil + + def self.exists?(filename,extensions=[]) + return false if nil_or_empty?(filename) + eachPathFor(filename) { |path| + return true if safeExists?(path) + for ext in extensions + return true if safeExists?(path+ext) + end + } + return false + end + + def self.getImagePath(filename) + return self.getPath(filename,["",".png",".gif"]) # ".jpg", ".jpeg", ".bmp" + end + + def self.getAudioPath(filename) + return self.getPath(filename,["",".mp3",".wav",".wma",".mid",".ogg",".midi"]) + end + + def self.getPath(filename,extensions=[]) + return filename if nil_or_empty?(filename) + eachPathFor(filename) { |path| + return path if safeExists?(path) + for ext in extensions + file = path+ext + return file if safeExists?(file) + end + } + return filename + end + + # Gets the absolute RGSS paths for the given file name + def self.eachPathFor(filename) + return if !filename + if filename[/^[A-Za-z]\:[\/\\]/] || filename[/^[\/\\]/] + # filename is already absolute + yield filename + else + # relative path + RTP.eachPath { |path| + if path=="./" + yield filename + else + yield path+filename + end + } + end + end + + # Gets all RGSS search paths. + # This function basically does nothing now, because + # the passage of time and introduction of MKXP make + # it useless, but leaving it for compatibility + # reasons + def self.eachPath + # XXX: Use "." instead of Dir.pwd because of problems retrieving files if + # the current directory contains an accent mark + yield ".".gsub(/[\/\\]/,"/").gsub(/[\/\\]$/,"")+"/" + end + + private + + def self.getSaveFileName(fileName) + File.join(getSaveFolder, fileName) + end + + def self.getSaveFolder + # MKXP makes sure that this folder has been created + # once it starts. The location differs depending on + # the operating system: + # Windows: %APPDATA% + # Linux: $HOME/.local/share + # macOS (unsandboxed): $HOME/Library/Application Support + System.data_directory + end +end + + + +module FileTest + Image_ext = ['.png', '.gif'] # '.jpg', '.jpeg', '.bmp', + Audio_ext = ['.mp3', '.mid', '.midi', '.ogg', '.wav', '.wma'] + + def self.audio_exist?(filename) + return RTP.exists?(filename,Audio_ext) + end + + def self.image_exist?(filename) + return RTP.exists?(filename,Image_ext) + end +end + + + +# Used to determine whether a data file exists (rather than a graphics or +# audio file). Doesn't check RTP, but does check encrypted archives. + +# Note: pbGetFileChar checks anything added in MKXP's RTP setting, +# and matching mount points added through System.mount +def pbRgssExists?(filename) + if safeExists?("./Game.rgssad") + return pbGetFileChar(filename)!=nil + else + filename = canonicalize(filename) + return safeExists?(filename) + end +end + +# Opens an IO, even if the file is in an encrypted archive. +# Doesn't check RTP for the file. + +# Note: load_data checks anything added in MKXP's RTP setting, +# and matching mount points added through System.mount +def pbRgssOpen(file,mode=nil) + #File.open("debug.txt","ab") { |fw| fw.write([file,mode,Time.now.to_f].inspect+"\r\n") } + if !safeExists?("./Game.rgssad") + if block_given? + File.open(file,mode) { |f| yield f } + return nil + else + return File.open(file,mode) + end + end + file = canonicalize(file) + Marshal.neverload = true + str = load_data(file, true) + if block_given? + StringInput.open(str) { |f| yield f } + return nil + else + return StringInput.open(str) + end +end + +# Gets at least the first byte of a file. Doesn't check RTP, but does check +# encrypted archives. +def pbGetFileChar(file) + canon_file = canonicalize(file) + if !safeExists?("./Game.rgssad") + return nil if !safeExists?(canon_file) + return nil if file.last == '/' # Is a directory + begin + File.open(canon_file, "rb") { |f| return f.read(1) } # read one byte + rescue Errno::ENOENT, Errno::EINVAL, Errno::EACCES, Errno::EISDIR + return nil + end + end + str = nil + begin + str = load_data(canon_file, true) + rescue Errno::ENOENT, Errno::EINVAL, Errno::EACCES, Errno::EISDIR, RGSSError, MKXPError + str = nil + end + return str +end + +def pbTryString(x) + ret = pbGetFileChar(x) + return (ret!=nil && ret!="") ? x : nil +end + +# Gets the contents of a file. Doesn't check RTP, but does check +# encrypted archives. + +# Note: load_data will check anything added in MKXP's RTP setting, +# and matching mount points added through System.mount +def pbGetFileString(file) + file = canonicalize(file) + if !safeExists?("./Game.rgssad") + return nil if !safeExists?(file) + begin + File.open(file,"rb") { |f| return f.read } # read all data + rescue Errno::ENOENT, Errno::EINVAL, Errno::EACCES + return nil + end + end + str = nil + begin + str = load_data(file, true) + rescue Errno::ENOENT, Errno::EINVAL, Errno::EACCES, RGSSError, MKXPError + str = nil + end + return str +end + + + +#=============================================================================== +# +#=============================================================================== +class StringInput + include Enumerable + + class << self + def new( str ) + if block_given? + begin + f = super + yield f + ensure + f.close if f + end + else + super + end + end + alias open new + end + + def initialize( str ) + @string = str + @pos = 0 + @closed = false + @lineno = 0 + end + + attr_reader :lineno,:string + + def inspect + return "#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@string[0,30].inspect}>" + end + + def close + raise IOError, 'closed stream' if @closed + @pos = nil + @closed = true + end + + def closed?; @closed; end + + def pos + raise IOError, 'closed stream' if @closed + [@pos, @string.size].min + end + + alias tell pos + + def rewind; seek(0); end + + def pos=(value); seek(value); end + + def seek(offset, whence=IO::SEEK_SET) + raise IOError, 'closed stream' if @closed + case whence + when IO::SEEK_SET then @pos = offset + when IO::SEEK_CUR then @pos += offset + when IO::SEEK_END then @pos = @string.size - offset + else + raise ArgumentError, "unknown seek flag: #{whence}" + end + @pos = 0 if @pos < 0 + @pos = [@pos, @string.size + 1].min + offset + end + + def eof? + raise IOError, 'closed stream' if @closed + @pos > @string.size + end + + def each( &block ) + raise IOError, 'closed stream' if @closed + begin + @string.each(&block) + ensure + @pos = 0 + end + end + + def gets + raise IOError, 'closed stream' if @closed + if idx = @string.index(?\n, @pos) + idx += 1 # "\n".size + line = @string[ @pos ... idx ] + @pos = idx + @pos += 1 if @pos == @string.size + else + line = @string[ @pos .. -1 ] + @pos = @string.size + 1 + end + @lineno += 1 + line + end + + def getc + raise IOError, 'closed stream' if @closed + ch = @string[@pos] + @pos += 1 + @pos += 1 if @pos == @string.size + ch + end + + def read( len = nil ) + raise IOError, 'closed stream' if @closed + if !len + return nil if eof? + rest = @string[@pos ... @string.size] + @pos = @string.size + 1 + return rest + end + str = @string[@pos, len] + @pos += len + @pos += 1 if @pos == @string.size + str + end + + def read_all; read(); end + + alias sysread read +end diff --git a/Data/Scripts/001_Technical/002_Files/002_FileMixins.rb b/Data/Scripts/001_Technical/002_Files/002_FileMixins.rb new file mode 100644 index 000000000..187049ec6 --- /dev/null +++ b/Data/Scripts/001_Technical/002_Files/002_FileMixins.rb @@ -0,0 +1,150 @@ +module FileInputMixin + def fgetb + ret = 0 + each_byte do |i| + ret = i || 0 + break + end + return ret + end + + def fgetw + x = 0 + ret = 0 + each_byte do |i| + break if !i + ret |= (i << x) + x += 8 + break if x == 16 + end + return ret + end + + def fgetdw + x = 0 + ret = 0 + each_byte do |i| + break if !i + ret |= (i << x) + x += 8 + break if x == 32 + end + return ret + end + + def fgetsb + ret = fgetb + ret -= 256 if (ret & 0x80) != 0 + return ret + end + + def xfgetb(offset) + self.pos = offset + return fgetb + end + + def xfgetw(offset) + self.pos = offset + return fgetw + end + + def xfgetdw(offset) + self.pos = offset + return fgetdw + end + + def getOffset(index) + self.binmode + self.pos = 0 + offset = fgetdw >> 3 + return 0 if index >= offset + self.pos = index * 8 + return fgetdw + end + + def getLength(index) + self.binmode + self.pos = 0 + offset = fgetdw >> 3 + return 0 if index >= offset + self.pos = index * 8 + 4 + return fgetdw + end + + def readName(index) + self.binmode + self.pos = 0 + offset = fgetdw >> 3 + return "" if index >= offset + self.pos = index << 3 + offset = fgetdw + length = fgetdw + return "" if length == 0 + self.pos = offset + return read(length) + end +end + +module FileOutputMixin + def fputb(b) + b &= 0xFF + write(b.chr) + end + + def fputw(w) + 2.times do + b = w & 0xFF + write(b.chr) + w >>= 8 + end + end + + def fputdw(w) + 4.times do + b = w & 0xFF + write(b.chr) + w >>= 8 + end + end +end + +class File < IO +=begin + unless defined?(debugopen) + class << self + alias debugopen open + end + end + + def open(f, m = "r") + debugopen("debug.txt", "ab") { |file| file.write([f, m, Time.now.to_f].inspect + "\r\n") } + if block_given? + debugopen(f, m) { |file| yield file } + else + return debugopen(f, m) + end + end +=end + include FileInputMixin + include FileOutputMixin +end + +class StringInput + include FileInputMixin + + def pos=(value) + seek(value) + end + + def each_byte + while !eof? + yield getc + end + end + + def binmode; end +end + +class StringOutput + include FileOutputMixin +end diff --git a/Data/Scripts/001_Technical/002_Files/003_HTTP_Utilities.rb b/Data/Scripts/001_Technical/002_Files/003_HTTP_Utilities.rb new file mode 100644 index 000000000..b918e0592 --- /dev/null +++ b/Data/Scripts/001_Technical/002_Files/003_HTTP_Utilities.rb @@ -0,0 +1,150 @@ +############################# +# +# HTTP utility functions +# +############################# +# + +def pbPostData(url, postdata, filename=nil, depth=0) + if url[/^http:\/\/([^\/]+)(.*)$/] + host = $1 + path = $2 + path = "/" if path.length==0 + userAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14" + body = postdata.map { |key, value| + keyString = key.to_s + valueString = value.to_s + keyString.gsub!(/[^a-zA-Z0-9_\.\-]/n) { |s| sprintf('%%%02x', s[0]) } + valueString.gsub!(/[^a-zA-Z0-9_\.\-]/n) { |s| sprintf('%%%02x', s[0]) } + next "#{keyString}=#{valueString}" + }.join('&') + ret = HTTPLite.post_body( + url, + body, + "application/x-www-form-urlencoded", + { + "Host" => host, # might not be necessary + "Proxy-Connection" => "Close", + "Content-Length" => body.bytesize.to_s, + "Pragma" => "no-cache", + "User-Agent" => userAgent + } + ) rescue "" + return ret if !ret.is_a?(Hash) + return "" if ret[:status] != 200 + return ret[:body] if !filename + File.open(filename, "wb"){|f|f.write(ret[:body])} + return "" + end + return "" +end + +def pbDownloadData(url, filename = nil, authorization = nil, depth = 0, &block) + return nil if !downloadAllowed?() + echoln "downloading data from #{url}" + headers = { + "Proxy-Connection" => "Close", + "Pragma" => "no-cache", + "User-Agent" => "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14" + } + headers["authorization"] = authorization if authorization + ret = HTTPLite.get(url, headers) rescue "" + return ret if !ret.is_a?(Hash) + return "" if ret[:status] != 200 + return ret[:body] if !filename + File.open(filename, "wb") { |f| f.write(ret[:body]) } + return "" +end + +def pbDownloadToString(url) + begin + data = pbDownloadData(url) + return data if data + return "" + rescue + return "" + end +end + +def pbDownloadToFile(url, file) + begin + pbDownloadData(url,file) + rescue + end +end + +def pbPostToString(url, postdata) + begin + data = pbPostData(url, postdata) + return data + rescue + return "" + end +end + +def pbPostToFile(url, postdata, file) + begin + pbPostData(url, postdata,file) + rescue + end +end + +def serialize_value(value) + if value.is_a?(Hash) + serialize_json(value) + elsif value.is_a?(String) + escaped_value = value.gsub(/\\/, '\\\\\\').gsub(/"/, '\\"').gsub(/\n/, '\\n').gsub(/\r/, '\\r') + "\"#{escaped_value}\"" + else + value.to_s + end +end + + +def serialize_json(data) + #echoln data + # Manually serialize the JSON data into a string + parts = ["{"] + data.each_with_index do |(key, value), index| + parts << "\"#{key}\":#{serialize_value(value)}" + parts << "," unless index == data.size - 1 + end + parts << "}" + return parts.join +end + + +def downloadAllowed?() + return $PokemonSystem.download_sprites==0 +end + +def clean_json_string(str) + #echoln str + #return str if $PokemonSystem.on_mobile + # Remove non-UTF-8 characters and unexpected control characters + #cleaned_str = str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') + cleaned_str = str + # Remove literal \n, \r, \t, etc. + cleaned_str = cleaned_str.gsub(/\\n|\\r|\\t/, '') + + # Remove actual newlines and carriage returns + cleaned_str = cleaned_str.gsub(/[\n\r]/, '') + + # Remove leading and trailing quotes + cleaned_str = cleaned_str.gsub(/\A"|"\Z/, '') + + # Replace Unicode escape sequences with corresponding characters + cleaned_str = cleaned_str.gsub(/\\u([\da-fA-F]{4})/) { |match| + [$1.to_i(16)].pack("U") + } + return cleaned_str +end + + + + + + + + + diff --git a/Data/Scripts/001_Technical/002_RubyUtilities.rb b/Data/Scripts/001_Technical/002_RubyUtilities.rb new file mode 100644 index 000000000..c71bb3e06 --- /dev/null +++ b/Data/Scripts/001_Technical/002_RubyUtilities.rb @@ -0,0 +1,142 @@ +#=============================================================================== +# class Object +#=============================================================================== +class Object + alias full_inspect inspect unless method_defined?(:full_inspect) + + def inspect + return "#<#{self.class}>" + end +end + +#=============================================================================== +# class Class +#=============================================================================== +class Class + def to_sym + return self.to_s.to_sym + end +end + +#=============================================================================== +# class String +#=============================================================================== +class String + def starts_with_vowel? + return ['a', 'e', 'i', 'o', 'u'].include?(self[0, 1].downcase) + end + + def first(n = 1) + return self[0...n] + end + + def last(n = 1) + return self[-n..-1] || self + end + + def blank? + blank = true + s = self.scan(/./) + for l in s + blank = false if l != "" + end + return blank + end + + def cut(bitmap, width) + string = self + width -= bitmap.text_size("...").width + string_width = 0 + text = [] + for char in string.scan(/./) + wdh = bitmap.text_size(char).width + next if (wdh + string_width) > width + string_width += wdh + text.push(char) + end + text.push("...") if text.length < string.length + new_string = "" + for char in text + new_string += char + end + return new_string + end +end + +#=============================================================================== +# class Numeric +#=============================================================================== +class Numeric + # Turns a number into a string formatted like 12,345,678. + def to_s_formatted + return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse + end + + def to_word + ret = [_INTL("zero"), _INTL("one"), _INTL("two"), _INTL("three"), + _INTL("four"), _INTL("five"), _INTL("six"), _INTL("seven"), + _INTL("eight"), _INTL("nine"), _INTL("ten"), _INTL("eleven"), + _INTL("twelve"), _INTL("thirteen"), _INTL("fourteen"), _INTL("fifteen"), + _INTL("sixteen"), _INTL("seventeen"), _INTL("eighteen"), _INTL("nineteen"), + _INTL("twenty")] + return ret[self] if self.is_a?(Integer) && self >= 0 && self <= ret.length + return self.to_s + end +end + +#=============================================================================== +# class Array +#=============================================================================== +class Array + def ^(other) # xor of two arrays + return (self|other) - (self&other) + end + + def swap(val1, val2) + index1 = self.index(val1) + index2 = self.index(val2) + self[index1] = val2 + self[index2] = val1 + end +end + +#=============================================================================== +# module Enumerable +#=============================================================================== +module Enumerable + def transform + ret = [] + self.each { |item| ret.push(yield(item)) } + return ret + end +end + +#=============================================================================== +# Kernel methods +#=============================================================================== +def rand(*args) + Kernel.rand(*args) +end + +class << Kernel + alias oldRand rand unless method_defined?(:oldRand) + def rand(a = nil, b = nil) + if a.is_a?(Range) + lo = a.min + hi = a.max + return lo + oldRand(hi - lo + 1) + elsif a.is_a?(Numeric) + if b.is_a?(Numeric) + return a + oldRand(b - a + 1) + else + return oldRand(a) + end + elsif a.nil? + return (b) ? oldRand(b) : oldRand(2) + end + end +end + +def nil_or_empty?(string) + return string.nil? || !string.is_a?(String) || string.size == 0 +end diff --git a/Data/Scripts/001_Technical/003_Intl_Messages.rb b/Data/Scripts/001_Technical/003_Intl_Messages.rb new file mode 100644 index 000000000..7f8b21799 --- /dev/null +++ b/Data/Scripts/001_Technical/003_Intl_Messages.rb @@ -0,0 +1,789 @@ +def pbAddScriptTexts(items,script) + script.scan(/(?:_I)\s*\(\s*\"((?:[^\\\"]*\\\"?)*[^\"]*)\"/) { |s| + string=s[0] + string.gsub!(/\\\"/,"\"") + string.gsub!(/\\\\/,"\\") + items.push(string) + } +end + +def pbAddRgssScriptTexts(items,script) + script.scan(/(?:_INTL|_ISPRINTF)\s*\(\s*\"((?:[^\\\"]*\\\"?)*[^\"]*)\"/) { |s| + string=s[0] + string.gsub!(/\\r/,"\r") + string.gsub!(/\\n/,"\n") + string.gsub!(/\\1/,"\1") + string.gsub!(/\\\"/,"\"") + string.gsub!(/\\\\/,"\\") + items.push(string) + } +end + +def pbSetTextMessages + Graphics.update + begin + t = Time.now.to_i + texts=[] + for script in $RGSS_SCRIPTS + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + scr=Zlib::Inflate.inflate(script[2]) + pbAddRgssScriptTexts(texts,scr) + end + if safeExists?("Data/PluginScripts.rxdata") + plugin_scripts = load_data("Data/PluginScripts.rxdata") + for plugin in plugin_scripts + for script in plugin[2] + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + scr = Zlib::Inflate.inflate(script[1]).force_encoding(Encoding::UTF_8) + pbAddRgssScriptTexts(texts,scr) + end + end + end + # Must add messages because this code is used by both game system and Editor + MessageTypes.addMessagesAsHash(MessageTypes::ScriptTexts,texts) + commonevents = load_data("Data/CommonEvents.rxdata") + items=[] + choices=[] + for event in commonevents.compact + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + begin + neednewline=false + lastitem="" + for j in 0...event.list.size + list = event.list[j] + if neednewline && list.code!=401 + if lastitem!="" + lastitem.gsub!(/([^\.\!\?])\s\s+/) { |m| $1+" " } + items.push(lastitem) + lastitem="" + end + neednewline=false + end + if list.code == 101 + lastitem+="#{list.parameters[0]}" + neednewline=true + elsif list.code == 102 + for k in 0...list.parameters[0].length + choices.push(list.parameters[0][k]) + end + neednewline=false + elsif list.code == 401 + lastitem+=" " if lastitem!="" + lastitem+="#{list.parameters[0]}" + neednewline=true + elsif list.code == 355 || list.code == 655 + pbAddScriptTexts(items,list.parameters[0]) + elsif list.code == 111 && list.parameters[0]==12 + pbAddScriptTexts(items,list.parameters[1]) + elsif list.code == 209 + route=list.parameters[1] + for k in 0...route.list.size + if route.list[k].code == 45 + pbAddScriptTexts(items,route.list[k].parameters[0]) + end + end + end + end + if neednewline + if lastitem!="" + items.push(lastitem) + lastitem="" + end + end + end + end + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + items|=[] + choices|=[] + items.concat(choices) + MessageTypes.setMapMessagesAsHash(0,items) + mapinfos = pbLoadMapInfos + mapnames=[] + for id in mapinfos.keys + mapnames[id]=mapinfos[id].name + end + MessageTypes.setMessages(MessageTypes::MapNames,mapnames) + for id in mapinfos.keys + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + filename=sprintf("Data/Map%03d.rxdata",id) + next if !pbRgssExists?(filename) + map = load_data(filename) + items=[] + choices=[] + for event in map.events.values + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + begin + for i in 0...event.pages.size + neednewline=false + lastitem="" + for j in 0...event.pages[i].list.size + list = event.pages[i].list[j] + if neednewline && list.code!=401 + if lastitem!="" + lastitem.gsub!(/([^\.\!\?])\s\s+/) { |m| $1+" " } + items.push(lastitem) + lastitem="" + end + neednewline=false + end + if list.code == 101 + lastitem+="#{list.parameters[0]}" + neednewline=true + elsif list.code == 102 + for k in 0...list.parameters[0].length + choices.push(list.parameters[0][k]) + end + neednewline=false + elsif list.code == 401 + lastitem+=" " if lastitem!="" + lastitem+="#{list.parameters[0]}" + neednewline=true + elsif list.code == 355 || list.code==655 + pbAddScriptTexts(items,list.parameters[0]) + elsif list.code == 111 && list.parameters[0]==12 + pbAddScriptTexts(items,list.parameters[1]) + elsif list.code==209 + route=list.parameters[1] + for k in 0...route.list.size + if route.list[k].code==45 + pbAddScriptTexts(items,route.list[k].parameters[0]) + end + end + end + end + if neednewline + if lastitem!="" + items.push(lastitem) + lastitem="" + end + end + end + end + end + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + items|=[] + choices|=[] + items.concat(choices) + MessageTypes.setMapMessagesAsHash(id,items) + if Time.now.to_i - t >= 5 + t = Time.now.to_i + Graphics.update + end + end + rescue Hangup + end + Graphics.update +end + +def pbEachIntlSection(file) + lineno=1 + re=/^\s*\[\s*([^\]]+)\s*\]\s*$/ + havesection=false + sectionname=nil + lastsection=[] + file.each_line { |line| + if lineno==1 && line[0].ord==0xEF && line[1].ord==0xBB && line[2].ord==0xBF + line=line[3,line.length-3] + end + if !line[/^\#/] && !line[/^\s*$/] + if line[re] + if havesection + yield lastsection,sectionname + end + lastsection.clear + sectionname=$~[1] + havesection=true + else + if sectionname==nil + raise _INTL("Expected a section at the beginning of the file (line {1})",lineno) + end + lastsection.push(line.gsub(/\s+$/,"")) + end + end + lineno+=1 + if lineno%500==0 + Graphics.update + end + } + if havesection + yield lastsection,sectionname + end +end + +def pbGetText(infile) + begin + file=File.open(infile,"rb") + rescue + raise _INTL("Can't find {1}",infile) + end + intldat=[] + begin + pbEachIntlSection(file) { |section,name| + next if section.length==0 + if !name[/^([Mm][Aa][Pp])?(\d+)$/] + raise _INTL("Invalid section name {1}",name) + end + ismap=$~[1] && $~[1]!="" + id=$~[2].to_i + itemlength=0 + if section[0][/^\d+$/] + intlhash=[] + itemlength=3 + if ismap + raise _INTL("Section {1} can't be an ordered list (section was recognized as an ordered list because its first line is a number)",name) + end + if section.length%3!=0 + raise _INTL("Section {1}'s line count is not divisible by 3 (section was recognized as an ordered list because its first line is a number)",name) + end + else + intlhash=OrderedHash.new + itemlength=2 + if section.length%2!=0 + raise _INTL("Section {1} has an odd number of entries (section was recognized as a hash because its first line is not a number)",name) + end + end + i=0 + loop do break unless i0 + str+=@keys[i].inspect+"=>"+self[@keys[i]].inspect + end + str+="}" + return str + end + + alias :to_s :inspect + + def []=(key,value) + oldvalue=self[key] + if !oldvalue && value + @keys.push(key) + elsif !value + @keys|=[] + @keys-=[key] + end + return super(key,value) + end + + def self._load(string) + ret=self.new + keysvalues=Marshal.load(string) + keys=keysvalues[0] + values=keysvalues[1] + for i in 0...keys.length + ret[keys[i]]=values[i] + end + return ret + end + + def _dump(_depth=100) + values=[] + for key in @keys + values.push(self[key]) + end + return Marshal.dump([@keys,values]) + end +end + + + +class Messages + def initialize(filename=nil,delayLoad=false) + @messages=nil + @filename=filename + if @filename && !delayLoad + loadMessageFile(@filename) + end + end + + def delayedLoad + if @filename && !@messages + loadMessageFile(@filename) + @filename=nil + end + end + + def self.stringToKey(str) + if str && str[/[\r\n\t\1]|^\s+|\s+$|\s{2,}/] + key=str.clone + key.gsub!(/^\s+/,"") + key.gsub!(/\s+$/,"") + key.gsub!(/\s{2,}/," ") + return key + end + return str + end + + def self.normalizeValue(value) + if value[/[\r\n\t\x01]|^[\[\]]/] + ret=value.clone + ret.gsub!(/\r/,"<>") + ret.gsub!(/\n/,"<>") + ret.gsub!(/\t/,"<>") + ret.gsub!(/\[/,"<<[>>") + ret.gsub!(/\]/,"<<]>>") + ret.gsub!(/\x01/,"<<1>>") + return ret + end + return value + end + + def self.denormalizeValue(value) + if value[/<<[rnt1\[\]]>>/] + ret=value.clone + ret.gsub!(/<<1>>/,"\1") + ret.gsub!(/<>/,"\r") + ret.gsub!(/<>/,"\n") + ret.gsub!(/<<\[>>/,"[") + ret.gsub!(/<<\]>>/,"]") + ret.gsub!(/<>/,"\t") + return ret + end + return value + end + + def self.writeObject(f,msgs,secname,origMessages=nil) + return if !msgs + if msgs.is_a?(Array) + f.write("[#{secname}]\r\n") + for j in 0...msgs.length + next if nil_or_empty?(msgs[j]) + value=Messages.normalizeValue(msgs[j]) + origValue="" + if origMessages + origValue=Messages.normalizeValue(origMessages.get(secname,j)) + else + origValue=Messages.normalizeValue(MessageTypes.get(secname,j)) + end + f.write("#{j}\r\n") + f.write(origValue+"\r\n") + f.write(value+"\r\n") + end + elsif msgs.is_a?(OrderedHash) + f.write("[#{secname}]\r\n") + keys=msgs.keys + for key in keys + next if nil_or_empty?(msgs[key]) + value=Messages.normalizeValue(msgs[key]) + valkey=Messages.normalizeValue(key) + # key is already serialized + f.write(valkey+"\r\n") + f.write(value+"\r\n") + end + end + end + + def messages + return @messages || [] + end + + def extract(outfile) +# return if !@messages + origMessages=Messages.new("Data/messages.dat") + File.open(outfile,"wb") { |f| + f.write(0xef.chr) + f.write(0xbb.chr) + f.write(0xbf.chr) + f.write("# To localize this text for a particular language, please\r\n") + f.write("# translate every second line of this file.\r\n") + if origMessages.messages[0] + for i in 0...origMessages.messages[0].length + msgs=origMessages.messages[0][i] + Messages.writeObject(f,msgs,"Map#{i}",origMessages) + end + end + for i in 1...origMessages.messages.length + msgs=origMessages.messages[i] + Messages.writeObject(f,msgs,i,origMessages) + end + } + end + + def setMessages(type,array) + @messages=[] if !@messages + arr=[] + for i in 0...array.length + arr[i]=(array[i]) ? array[i] : "" + end + @messages[type]=arr + end + + def addMessages(type,array) + @messages=[] if !@messages + arr=(@messages[type]) ? @messages[type] : [] + for i in 0...array.length + arr[i]=(array[i]) ? array[i] : (arr[i]) ? arr[i] : "" + end + @messages[type]=arr + end + + def self.createHash(_type,array) + arr=OrderedHash.new + for i in 0...array.length + if array[i] + key=Messages.stringToKey(array[i]) + arr[key]=array[i] + end + end + return arr + end + + def self.addToHash(_type,array,hash) + hash=OrderedHash.new if !hash + for i in 0...array.length + if array[i] + key=Messages.stringToKey(array[i]) + hash[key]=array[i] + end + end + return hash + end + + def setMapMessagesAsHash(type,array) + @messages=[] if !@messages + @messages[0]=[] if !@messages[0] + @messages[0][type]=Messages.createHash(type,array) + end + + def addMapMessagesAsHash(type,array) + @messages=[] if !@messages + @messages[0]=[] if !@messages[0] + @messages[0][type]=Messages.addToHash(type,array,@messages[0][type]) + end + + def setMessagesAsHash(type,array) + @messages=[] if !@messages + @messages[type]=Messages.createHash(type,array) + end + + def addMessagesAsHash(type,array) + @messages=[] if !@messages + @messages[type]=Messages.addToHash(type,array,@messages[type]) + end + + def saveMessages(filename=nil) + filename="Data/messages.dat" if !filename + File.open(filename,"wb") { |f| Marshal.dump(@messages,f) } + end + + def loadMessageFile(filename) + begin + pbRgssOpen(filename,"rb") { |f| @messages=Marshal.load(f) } + if !@messages.is_a?(Array) + @messages=nil + raise "Corrupted data" + end + return @messages + rescue + @messages=nil + return nil + end + end + + def set(type,id,value) + delayedLoad + return if !@messages + return if !@messages[type] + @messages[type][id]=value + end + + def getCount(type) + delayedLoad + return 0 if !@messages + return 0 if !@messages[type] + return @messages[type].length + end + + def get(type,id) + delayedLoad + return "" if !@messages + return "" if !@messages[type] + return "" if !@messages[type][id] + return @messages[type][id] + end + + def getFromHash(type,key) + delayedLoad + return key if !@messages || !@messages[type] || !key + id=Messages.stringToKey(key) + return key if !@messages[type][id] + return @messages[type][id] + end + + def getFromMapHash(type,key) + delayedLoad + return key if !@messages + return key if !@messages[0] + return key if !@messages[0][type] && !@messages[0][0] + id=Messages.stringToKey(key) + if @messages[0][type] && @messages[0][type][id] + return @messages[0][type][id] + elsif @messages[0][0] && @messages[0][0][id] + return @messages[0][0][id] + end + return key + end +end + + + +module MessageTypes + # Value 0 is used for common event and map event text + Species = 1 + Kinds = 2 + Entries = 3 + FormNames = 4 + Moves = 5 + MoveDescriptions = 6 + Items = 7 + ItemPlurals = 8 + ItemDescriptions = 9 + Abilities = 10 + AbilityDescs = 11 + Types = 12 + TrainerTypes = 13 + TrainerNames = 14 + BeginSpeech = 15 + EndSpeechWin = 16 + EndSpeechLose = 17 + RegionNames = 18 + PlaceNames = 19 + PlaceDescriptions = 20 + MapNames = 21 + PhoneMessages = 22 + TrainerLoseText = 23 + ScriptTexts = 24 + RibbonNames = 25 + RibbonDescriptions = 26 + @@messages = Messages.new + @@messagesFallback = Messages.new("Data/messages.dat",true) + + def self.stringToKey(str) + return Messages.stringToKey(str) + end + + def self.normalizeValue(value) + return Messages.normalizeValue(value) + end + + def self.denormalizeValue(value) + Messages.denormalizeValue(value) + end + + def self.writeObject(f,msgs,secname) + Messages.denormalizeValue(str) + end + + def self.extract(outfile) + @@messages.extract(outfile) + end + + def self.setMessages(type,array) + @@messages.setMessages(type,array) + end + + def self.addMessages(type,array) + @@messages.addMessages(type,array) + end + + def self.createHash(type,array) + Messages.createHash(type,array) + end + + def self.addMapMessagesAsHash(type,array) + @@messages.addMapMessagesAsHash(type,array) + end + + def self.setMapMessagesAsHash(type,array) + @@messages.setMapMessagesAsHash(type,array) + end + + def self.addMessagesAsHash(type,array) + @@messages.addMessagesAsHash(type,array) + end + + def self.setMessagesAsHash(type,array) + @@messages.setMessagesAsHash(type,array) + end + + def self.saveMessages(filename=nil) + @@messages.saveMessages(filename) + end + + def self.loadMessageFile(filename) + @@messages.loadMessageFile(filename) + end + + def self.get(type,id) + ret=@@messages.get(type,id) + if ret=="" + ret=@@messagesFallback.get(type,id) + end + return ret + end + + def self.getCount(type) + c1=@@messages.getCount(type) + c2=@@messagesFallback.getCount(type) + return c1>c2 ? c1 : c2 + end + + def self.getOriginal(type,id) + return @@messagesFallback.get(type,id) + end + + def self.getFromHash(type,key) + @@messages.getFromHash(type,key) + end + + def self.getFromMapHash(type,key) + @@messages.getFromMapHash(type,key) + end +end + + + +def pbLoadMessages(file) + return MessageTypes.loadMessageFile(file) +end + +def pbGetMessageCount(type) + return MessageTypes.getCount(type) +end + +def pbGetMessage(type,id) + return MessageTypes.get(type,id) +end + +def pbGetMessageFromHash(type,id) + return MessageTypes.getFromHash(type,id) +end + +# Replaces first argument with a localized version and formats the other +# parameters by replacing {1}, {2}, etc. with those placeholders. +def _INTL(*arg) + begin + string=MessageTypes.getFromHash(MessageTypes::ScriptTexts,arg[0]) + rescue + string=arg[0] + end + string=string.clone + for i in 1...arg.length + string.gsub!(/\{#{i}\}/,"#{arg[i]}") + end + return string +end + +# Replaces first argument with a localized version and formats the other +# parameters by replacing {1}, {2}, etc. with those placeholders. +# This version acts more like sprintf, supports e.g. {1:d} or {2:s} +def _ISPRINTF(*arg) + begin + string=MessageTypes.getFromHash(MessageTypes::ScriptTexts,arg[0]) + rescue + string=arg[0] + end + string=string.clone + for i in 1...arg.length + string.gsub!(/\{#{i}\:([^\}]+?)\}/) { |m| + next sprintf("%"+$1,arg[i]) + } + end + return string +end + +def _I(str) + return _MAPINTL($game_map.map_id,str) +end + +def _MAPINTL(mapid,*arg) + string=MessageTypes.getFromMapHash(mapid,arg[0]) + string=string.clone + for i in 1...arg.length + string.gsub!(/\{#{i}\}/,"#{arg[i]}") + end + return string +end + +def _MAPISPRINTF(mapid,*arg) + string=MessageTypes.getFromMapHash(mapid,arg[0]) + string=string.clone + for i in 1...arg.length + string.gsub!(/\{#{i}\:([^\}]+?)\}/) { |m| + next sprintf("%"+$1,arg[i]) + } + end + return string +end diff --git a/Data/Scripts/001_Technical/004_Input.rb b/Data/Scripts/001_Technical/004_Input.rb new file mode 100644 index 000000000..952965d7b --- /dev/null +++ b/Data/Scripts/001_Technical/004_Input.rb @@ -0,0 +1,33 @@ +module Input + USE = C + BACK = B + ACTION = A + JUMPUP = X + JUMPDOWN = Y + SPECIAL = Z + AUX1 = L + AUX2 = R + + unless defined?(update_KGC_ScreenCapture) + class << Input + alias update_KGC_ScreenCapture update + end + end + + def self.update + update_KGC_ScreenCapture + if trigger?(Input::F8) + pbScreenCapture + end + end +end + +module Mouse + module_function + + # Returns the position of the mouse relative to the game window. + def getMousePos(catch_anywhere = false) + return nil unless Input.mouse_in_window || catch_anywhere + return Input.mouse_x, Input.mouse_y + end +end diff --git a/Data/Scripts/001_Technical/005_PluginManager.rb b/Data/Scripts/001_Technical/005_PluginManager.rb new file mode 100644 index 000000000..befca88ce --- /dev/null +++ b/Data/Scripts/001_Technical/005_PluginManager.rb @@ -0,0 +1,712 @@ +#==============================================================================# +# Plugin Manager # +# by Marin # +# support for external plugin scripts by Luka S.J. # +# tweaked by Maruno # +#------------------------------------------------------------------------------# +# Provides a simple interface that allows plugins to require dependencies # +# at specific versions, and to specify incompatibilities between plugins. # +# # +# Supports external scripts that are in .rb files in folders within the # +# Plugins folder. # +#------------------------------------------------------------------------------# +# Usage: # +# # +# A Pokémon Essentials plugin should register itself using the PluginManager. # +# The simplest way to do so, for a plugin without dependencies, is as follows: # +# # +# PluginManager.register({ # +# :name => "Basic Plugin", # +# :version => "1.0", # +# :link => "https://reliccastle.com/link-to-the-plugin/", # +# :credits => "Marin" # +# }) # +# # +# The link portion here is optional, but recommended. This will be shown in # +# the error message if the PluginManager detects that this plugin needs to be # +# updated. # +# # +# A plugin's version should be in the format X.Y.Z, but the number of digits # +# you use does not matter. You can also use Xa, Xb, Xc, Ya, etc. # +# What matters is that you use it consistently, so that it can be compared. # +# # +# IF there are multiple people to credit, their names should be in an array. # +# If there is only one credit, it does not need an array: # +# # +# :credits => "Marin" # +# :credits => ["Marin", "Maruno"], # +# # +# # +# # +# Dependency: # +# # +# A plugin can require another plugin to be installed in order to work. For # +# example, the "Simple Extension" plugin depends on the above "Basic Plugin" # +# like so: # +# # +# PluginManager.register({ # +# :name => "Simple Extension", # +# :version => "1.0", # +# :link => "https://reliccastle.com/link-to-the-plugin/", # +# :credits => ["Marin", "Maruno"], # +# :dependencies => ["Basic Plugin"] # +# }) # +# # +# If there are multiple dependencies, they should be listed in an array. If # +# there is only one dependency, it does not need an array: # +# # +# :dependencies => "Basic Plugin" # +# # +# To require a minimum version of a dependency plugin, you should turn the # +# dependency's name into an array which contains the name and the version # +# (both as strings). For example, to require "Basic Plugin" version 1.2 or # +# higher, you would write: # +# # +# :dependencies => [ # +# ["Basic Plugin", "1.2"] # +# ] # +# # +# To require a specific version (no higher and no lower) of a dependency # +# plugin, you should add the :exact flag as the first thing in the array for # +# that dependency: # +# # +# :dependencies => [ # +# [:exact, "Basic Plugin", "1.2"] # +# ] # +# # +# If your plugin can work without another plugin, but it is incompatible with # +# an old version of that other plugin, you should list it as an optional # +# dependency. If that other plugin is present in a game, then this optional # +# dependency will check whether it meets the minimum version required for your # +# plugin. Write it in the same way as any other dependency as described above, # +# but use the :optional flag instead. # +# # +# :dependencies => [ # +# [:optional, "QoL Improvements", "1.1"] # +# ] # +# # +# The :optional_exact flag is a combination of :optional and :exact. # +# # +# # +# # +# Incompatibility: # +# # +# If your plugin is known to be incompatible with another plugin, you should # +# list that other plugin as such. Only one of the two plugins needs to list # +# that it is incompatible with the other. # +# # +# PluginManager.register({ # +# :name => "QoL Improvements", # +# :version => "1.0", # +# :link => "https://reliccastle.com/link-to-the-plugin/", # +# :credits => "Marin", # +# :incompatibilities => [ # +# "Simple Extension" # +# ] # +# }) # +# # +#------------------------------------------------------------------------------# +# Plugin folder: # +# # +# The Plugin folder is treated like the PBS folder, but for script files for # +# plugins. Each plugin has its own folder within the Plugin folder. Each # +# plugin must have a meta.txt file in its folder, which contains information # +# about that plugin. Folders without this meta.txt file are ignored. # +# # +# Scripts must be in .rb files. You should not put any other files into a # +# plugin's folder except for script files and meta.txt. # +# # +# When the game is compiled, scripts in these folders are read and converted # +# into a usable format, and saved in the file Data/PluginScripts.rxdata. # +# Script files are loaded in order of their name and subfolder, so it is wise # +# to name script files "001_first script.rb", "002_second script.rb", etc. to # +# ensure they are loaded in the correct order. # +# # +# When the game is compressed for distribution, the Plugin folder and all its # +# contents should be deleted (like the PBS folder), because its contents will # +# be unused (they will have been compiled into the PluginScripts.rxdata file). # +# # +# The contents of meta.txt are as follows: # +# # +# Name = Simple Extension # +# Version = 1.0 # +# Requires = Basic Plugin # +# Requires = Useful Utilities,1.1 # +# Conflicts = Complex Extension # +# Conflicts = Extended Windows # +# Link = https://reliccastle.com/link-to-the-plugin/ # +# Credits = Luka S.J.,Maruno,Marin # +# # +# These lines are related to what is described above. You can have multiple # +# "Requires" and "Conflicts" lines, each listing a single other plugin that is # +# either a dependency or a conflict respectively. # +# # +# Examples of the "Requires" line: # +# # +# Requires = Basic Plugin # +# Requires = Basic Plugin,1.1 # +# Requires = Basic Plugin,1.1,exact # +# Requires = Basic Plugin,1.1,optional # +# Exact = Basic Plugin,1.1 # +# Optional = Basic Plugin,1.1 # +# # +# The "Exact" and "Optional" lines are equivalent to the "Requires" lines # +# that contain those keywords. # +# # +# There is also a "Scripts" line, which lists one or more script files that # +# should be loaded first. You can have multiple "Scripts" lines. However, you # +# can achieve the same effect by simply naming your script files in # +# alphanumeric order to make them load in a particular order, so the "Scripts" # +# line should not be necessary. # +# # +#------------------------------------------------------------------------------# +# Please give credit when using this. # +#==============================================================================# + +module PluginManager + # Holds all registered plugin data. + @@Plugins = {} + #----------------------------------------------------------------------------- + # Registers a plugin and tests its dependencies and incompatibilities. + #----------------------------------------------------------------------------- + def self.register(options) + name = nil + version = nil + link = nil + dependencies = nil + incompats = nil + credits = [] + order = [:name, :version, :link, :dependencies, :incompatibilities, :credits] + # Ensure it first reads the plugin's name, which is used in error reporting, + # by sorting the keys + keys = options.keys.sort do |a, b| + idx_a = order.index(a) + idx_a = order.size if idx_a == -1 + idx_b = order.index(b) + idx_b = order.size if idx_b == -1 + next idx_a <=> idx_b + end + for key in keys + value = options[key] + case key + when :name # Plugin name + if nil_or_empty?(value) + self.error("Plugin name must be a non-empty string.") + end + if !@@Plugins[value].nil? + self.error("A plugin called '#{value}' already exists.") + end + name = value + when :version # Plugin version + if nil_or_empty?(value) + self.error("Plugin version must be a string.") + end + version = value + when :link # Plugin website + if nil_or_empty?(value) + self.error("Plugin link must be a non-empty string.") + end + link = value + when :dependencies # Plugin dependencies + dependencies = value + dependencies = [dependencies] if !dependencies.is_a?(Array) || !dependencies[0].is_a?(Array) + for dep in value + if dep.is_a?(String) # "plugin name" + if !self.installed?(dep) + self.error("Plugin '#{name}' requires plugin '#{dep}' to be installed above it.") + end + elsif dep.is_a?(Array) + case dep.size + when 1 # ["plugin name"] + if dep[0].is_a?(String) + dep_name = dep[0] + if !self.installed?(dep_name) + self.error("Plugin '#{name}' requires plugin '#{dep_name}' to be installed above it.") + end + else + self.error("Expected the plugin name as a string, but got #{dep[0].inspect}.") + end + when 2 # ["plugin name", "version"] + if dep[0].is_a?(Symbol) + self.error("A plugin version comparator symbol was given but no version was given.") + elsif dep[0].is_a?(String) && dep[1].is_a?(String) + dep_name = dep[0] + dep_version = dep[1] + next if self.installed?(dep_name, dep_version) + if self.installed?(dep_name) # Have plugin but lower version + msg = "Plugin '#{name}' requires plugin '#{dep_name}' version #{dep_version} or higher, " + + "but the installed version is #{self.version(dep_name)}." + if dep_link = self.link(dep_name) + msg += "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'." + end + self.error(msg) + else # Don't have plugin + self.error("Plugin '#{name}' requires plugin '#{dep_name}' version #{dep_version} " + + "or higher to be installed above it.") + end + end + when 3 # [:optional/:exact/:optional_exact, "plugin name", "version"] + if !dep[0].is_a?(Symbol) + self.error("Expected first dependency argument to be a symbol, but got #{dep[0].inspect}.") + end + if !dep[1].is_a?(String) + self.error("Expected second dependency argument to be a plugin name, but got #{dep[1].inspect}.") + end + if !dep[2].is_a?(String) + self.error("Expected third dependency argument to be the plugin version, but got #{dep[2].inspect}.") + end + dep_arg = dep[0] + dep_name = dep[1] + dep_version = dep[2] + optional = false + exact = false + case dep_arg + when :optional + optional = true + when :exact + exact = true + when :optional_exact + optional = true + exact = true + else + self.error("Expected first dependency argument to be one of " + + ":optional, :exact or :optional_exact, but got #{dep_arg.inspect}.") + end + if optional + if self.installed?(dep_name) && # Have plugin but lower version + !self.installed?(dep_name, dep_version, exact) + msg = "Plugin '#{name}' requires plugin '#{dep_name}', if installed, to be version #{dep_version}" + msg << " or higher" if !exact + msg << ", but the installed version was #{self.version(dep_name)}." + if dep_link = self.link(dep_name) + msg << "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'." + end + self.error(msg) + end + elsif !self.installed?(dep_name, dep_version, exact) + if self.installed?(dep_name) # Have plugin but lower version + msg = "Plugin '#{name}' requires plugin '#{dep_name}' to be version #{dep_version}" + msg << " or later" if !exact + msg << ", but the installed version was #{self.version(dep_name)}." + if dep_link = self.link(dep_name) + msg << "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'." + end + self.error(msg) + else # Don't have plugin + msg = "Plugin '#{name}' requires plugin '#{dep_name}' version #{dep_version} " + msg << "or later" if !exact + msg << "to be installed above it." + self.error(msg) + end + end + end + end + end + when :incompatibilities # Plugin incompatibilities + incompats = value + incompats = [incompats] if !incompats.is_a?(Array) + for incompat in incompats + if self.installed?(incompat) + self.error("Plugin '#{name}' is incompatible with '#{incompat}'. " + + "They cannot both be used at the same time.") + end + end + when :credits # Plugin credits + value = [value] if value.is_a?(String) + if value.is_a?(Array) + for entry in value + if !entry.is_a?(String) + self.error("Plugin '#{name}'s credits array contains a non-string value.") + else + credits << entry + end + end + else + self.error("Plugin '#{name}'s credits field must contain a string, or a string array.") + end + else + self.error("Invalid plugin registry key '#{key}'.") + end + end + for plugin in @@Plugins.values + if plugin[:incompatibilities] && plugin[:incompatibilities].include?(name) + self.error("Plugin '#{plugin[:name]}' is incompatible with '#{name}'. " + + "They cannot both be used at the same time.") + end + end + # Add plugin to class variable + @@Plugins[name] = { + :name => name, + :version => version, + :link => link, + :dependencies => dependencies, + :incompatibilities => incompats, + :credits => credits + } + end + #----------------------------------------------------------------------------- + # Throws a pure error message without stack trace or any other useless info. + #----------------------------------------------------------------------------- + def self.error(msg) + Graphics.update + t = Thread.new do + echoln "Plugin Error:\r\n#{msg}" + p "Plugin Error: #{msg}" + Thread.exit + end + while t.status + Graphics.update + end + Kernel.exit! true + end + #----------------------------------------------------------------------------- + # Returns true if the specified plugin is installed. + # If the version is specified, this version is taken into account. + # If mustequal is true, the version must be a match with the specified version. + #----------------------------------------------------------------------------- + def self.installed?(plugin_name, plugin_version = nil, mustequal = false) + plugin = @@Plugins[plugin_name] + return false if plugin.nil? + return true if plugin_version.nil? + comparison = compare_versions(plugin[:version], plugin_version) + return true if !mustequal && comparison >= 0 + return true if mustequal && comparison == 0 + end + #----------------------------------------------------------------------------- + # Returns the string names of all installed plugins. + #----------------------------------------------------------------------------- + def self.plugins + return @@Plugins.keys + end + #----------------------------------------------------------------------------- + # Returns the installed version of the specified plugin. + #----------------------------------------------------------------------------- + def self.version(plugin_name) + return if !installed?(plugin_name) + return @@Plugins[plugin_name][:version] + end + #----------------------------------------------------------------------------- + # Returns the link of the specified plugin. + #----------------------------------------------------------------------------- + def self.link(plugin_name) + return if !installed?(plugin_name) + return @@Plugins[plugin_name][:link] + end + #----------------------------------------------------------------------------- + # Returns the credits of the specified plugin. + #----------------------------------------------------------------------------- + def self.credits(plugin_name) + return if !installed?(plugin_name) + return @@Plugins[plugin_name][:credits] + end + #----------------------------------------------------------------------------- + # Compares two versions given in string form. v1 should be the plugin version + # you actually have, and v2 should be the minimum/desired plugin version. + # Return values: + # 1 if v1 is higher than v2 + # 0 if v1 is equal to v2 + # -1 if v1 is lower than v2 + #----------------------------------------------------------------------------- + def self.compare_versions(v1, v2) + d1 = v1.split("") + d1.insert(0, "0") if d1[0] == "." # Turn ".123" into "0.123" + while d1[-1] == "."; d1 = d1[0..-2]; end # Turn "123." into "123" + d2 = v2.split("") + d2.insert(0, "0") if d2[0] == "." # Turn ".123" into "0.123" + while d2[-1] == "."; d2 = d2[0..-2]; end # Turn "123." into "123" + for i in 0...[d1.size, d2.size].max # Compare each digit in turn + c1 = d1[i] + c2 = d2[i] + if c1 + return 1 if !c2 + return 1 if c1.to_i(16) > c2.to_i(16) + return -1 if c1.to_i(16) < c2.to_i(16) + else + return -1 if c2 + end + end + return 0 + end + #----------------------------------------------------------------------------- + # formats the error message + #----------------------------------------------------------------------------- + def self.pluginErrorMsg(name, script) + # begin message formatting + message = "[Infinite Fusion version #{Settings::GAME_VERSION_NUMBER}]\r\n" + message += "#{Essentials::ERROR_TEXT}\r\n" # For third party scripts to add to + message += "Error in Plugin [#{name}]:\r\n" + message += "#{$!.class} occurred.\r\n" + # go through message content + for line in $!.message.split("\r\n") + next if nil_or_empty?(line) + n = line[/\d+/] + err = line.split(":")[-1].strip + lms = line.split(":")[0].strip + err.gsub!(n, "") if n + err = err.capitalize if err.is_a?(String) && !err.empty? + linum = n ? "Line #{n}: " : "" + message += "#{linum}#{err}: #{lms}\r\n" + end + # show last 10 lines of backtrace + message += "\r\nBacktrace:\r\n" + $!.backtrace[0, 10].each { |i| message += "#{i}\r\n" } + # output to log + errorlog = "errorlog.txt" + errorlog = RTP.getSaveFileName("errorlog.txt") if (Object.const_defined?(:RTP) rescue false) + File.open(errorlog, "ab") do |f| + f.write("\r\n=================\r\n\r\n[#{Time.now}]\r\n") + f.write(message) + end + # format/censor the error log directory + errorlogline = errorlog.gsub("/", "\\") + errorlogline.sub!(Dir.pwd + "\\", "") + errorlogline.sub!(pbGetUserName, "USERNAME") + errorlogline = "\r\n" + errorlogline if errorlogline.length > 20 + # output message + print("#{message}\r\nThis exception was logged in #{errorlogline}.\r\nHold Ctrl when closing this message to copy it to the clipboard.") + # Give a ~500ms coyote time to start holding Control + t = System.delta + until (System.delta - t) >= 500000 + Input.update + if Input.press?(Input::CTRL) + Input.clipboard = message + break + end + end + end + #----------------------------------------------------------------------------- + # Used to read the metadata file + #----------------------------------------------------------------------------- + def self.readMeta(dir, file) + filename = "#{dir}/#{file}" + meta = {} + # read file + Compiler.pbCompilerEachPreppedLine(filename) { |line, line_no| + # split line up into property name and values + if !line[/^\s*(\w+)\s*=\s*(.*)$/] + raise _INTL("Bad line syntax (expected syntax like XXX=YYY)\r\n{1}", FileLineData.linereport) + end + property = $~[1].upcase + data = $~[2].split(',') + data.each_with_index { |value, i| data[i] = value.strip } + # begin formatting data hash + case property + when 'REQUIRES' + meta[:dependencies] = [] if !meta[:dependencies] + if data.length < 2 # No version given, just push name of plugin dependency + meta[:dependencies].push(data[0]) + next + elsif data.length == 2 # Push name and version of plugin dependency + meta[:dependencies].push([data[0], data[1]]) + else # Push dependency type, name and version of plugin dependency + meta[:dependencies].push([data[2].downcase.to_sym, data[0], data[1]]) + end + when 'EXACT' + next if data.length < 2 # Exact dependencies must have a version given; ignore if not + meta[:dependencies] = [] if !meta[:dependencies] + meta[:dependencies].push([:exact, data[0], data[1]]) + when 'OPTIONAL' + next if data.length < 2 # Optional dependencies must have a version given; ignore if not + meta[:dependencies] = [] if !meta[:dependencies] + meta[:dependencies].push([:optional, data[0], data[1]]) + when 'CONFLICTS' + meta[:incompatibilities] = [] if !meta[:incompatibilities] + data.each { |value| meta[:incompatibilities].push(value) if value && !value.empty? } + when 'SCRIPTS' + meta[:scripts] = [] if !meta[:scripts] + data.each { |scr| meta[:scripts].push(scr) } + when 'CREDITS' + meta[:credits] = data + when 'LINK', 'WEBSITE' + meta[:link] = data[0] + else + meta[property.downcase.to_sym] = data[0] + end + } + # generate a list of all script files to be loaded, in the order they are to + # be loaded (files listed in the meta file are loaded first) + meta[:scripts] = [] if !meta[:scripts] + # get all script files from plugin Dir + for fl in Dir.all(dir) + next if !fl.include?(".rb") + meta[:scripts].push(fl.gsub("#{dir}/", "")) + end + # ensure no duplicate script files are queued + meta[:scripts].uniq! + # return meta hash + return meta + end + #----------------------------------------------------------------------------- + # Get a list of all the plugin directories to inspect + #----------------------------------------------------------------------------- + def self.listAll + return [] + return [] if !$DEBUG || safeExists?("Game.rgssad") + # get a list of all directories in the `Plugins/` folder + dirs = [] + Dir.get("Plugins").each { |d| dirs.push(d) if Dir.safe?(d) } + # return all plugins + return dirs + end + #----------------------------------------------------------------------------- + # Catch any potential loop with dependencies and raise an error + #----------------------------------------------------------------------------- + def self.validateDependencies(name, meta, og = nil) + # exit if no registered dependency + return nil if !meta[name] || !meta[name][:dependencies] + og = [name] if !og + # go through all dependencies + for dname in meta[name][:dependencies] + # clean the name to a simple string + dname = dname[0] if dname.is_a?(Array) && dname.length == 2 + dname = dname[1] if dname.is_a?(Array) && dname.length == 3 + # catch looping dependency issue + self.error("Plugin '#{og[0]}' has looping dependencies which cannot be resolved automatically.") if !og.nil? && og.include?(dname) + new_og = og.clone + new_og.push(dname) + self.validateDependencies(dname, meta, new_og) + end + return name + end + #----------------------------------------------------------------------------- + # Sort load order based on dependencies (this ends up in reverse order) + #----------------------------------------------------------------------------- + def self.sortLoadOrder(order, plugins) + # go through the load order + for o in order + next if !plugins[o] || !plugins[o][:dependencies] + # go through all dependencies + for dname in plugins[o][:dependencies] + # clean the name to a simple string + dname = dname[0] if dname.is_a?(Array) && dname.length == 2 + dname = dname[1] if dname.is_a?(Array) && dname.length == 3 + # catch missing dependency + self.error("Plugin '#{o}' requires plugin '#{dname}' to work properly.") if !order.include?(dname) + # skip if already sorted + next if order.index(dname) > order.index(o) + # catch looping dependency issue + order.swap(o, dname) + order = self.sortLoadOrder(order, plugins) + end + end + return order + end + #----------------------------------------------------------------------------- + # Get the order in which to load plugins + #----------------------------------------------------------------------------- + def self.getPluginOrder + plugins = {} + order = [] + # Find all plugin folders that have a meta.txt and add them to the list of + # plugins. + for dir in self.listAll + # skip if there is no meta file + next if !safeExists?(dir + "/meta.txt") + ndx = order.length + meta = self.readMeta(dir, "meta.txt") + meta[:dir] = dir + # raise error if no name defined for plugin + self.error("No 'Name' metadata defined for plugin located at '#{dir}'.") if !meta[:name] + # raise error if no script defined for plugin + self.error("No 'Scripts' metadata defined for plugin located at '#{dir}'.") if !meta[:scripts] + plugins[meta[:name]] = meta + # raise error if a plugin with the same name already exists + self.error("A plugin called '#{meta[:name]}' already exists in the load order.") if order.include?(meta[:name]) + order.insert(ndx, meta[:name]) + end + # validate all dependencies + order.each { |o| self.validateDependencies(o, plugins) } + # sort the load order + return self.sortLoadOrder(order, plugins).reverse, plugins + end + #----------------------------------------------------------------------------- + # Check if plugins need compiling + #----------------------------------------------------------------------------- + def self.needCompiling?(order, plugins) + # fixed actions + return false if !$DEBUG || safeExists?("Game.rgssad") + return true if !safeExists?("Data/PluginScripts.rxdata") + Input.update + return true if Input.press?(Input::CTRL) + # analyze whether or not to push recompile + mtime = File.mtime("Data/PluginScripts.rxdata") + for o in order + # go through all the registered plugin scripts + scr = plugins[o][:scripts] + dir = plugins[o][:dir] + for sc in scr + return true if File.mtime("#{dir}/#{sc}") > mtime + end + return true if File.mtime("#{dir}/meta.txt") > mtime + end + return false + end + #----------------------------------------------------------------------------- + # Check if plugins need compiling + #----------------------------------------------------------------------------- + def self.compilePlugins(order, plugins) + echo 'Compiling plugin scripts...' + scripts = [] + # go through the entire order one by one + for o in order + # save name, metadata and scripts array + meta = plugins[o].clone + meta.delete(:scripts) + meta.delete(:dir) + dat = [o, meta, []] + # iterate through each file to deflate + for file in plugins[o][:scripts] + File.open("#{plugins[o][:dir]}/#{file}", 'rb') do |f| + dat[2].push([file, Zlib::Deflate.deflate(f.read)]) + end + end + # push to the main scripts array + scripts.push(dat) + end + # save to main `PluginScripts.rxdata` file + File.open("Data/PluginScripts.rxdata", 'wb') { |f| Marshal.dump(scripts, f) } + # collect garbage + GC.start + echoln ' done.' + echoln '' + end + #----------------------------------------------------------------------------- + # Check if plugins need compiling + #----------------------------------------------------------------------------- + def self.runPlugins + # get the order of plugins to interpret + order, plugins = self.getPluginOrder + # compile if necessary + self.compilePlugins(order, plugins) if self.needCompiling?(order, plugins) + # load plugins + scripts = load_data("Data/PluginScripts.rxdata") + echoed_plugins = [] + for plugin in scripts + # get the required data + name, meta, script = plugin + # register plugin + self.register(meta) + # go through each script and interpret + for scr in script + # turn code into plaintext + code = Zlib::Inflate.inflate(scr[1]).force_encoding(Encoding::UTF_8) + # get rid of tabs + code.gsub!("\t", " ") + # construct filename + sname = scr[0].gsub("\\","/").split("/")[-1] + fname = "[#{name}] #{sname}" + # try to run the code + begin + eval(code, TOPLEVEL_BINDING, fname) + echoln "Loaded plugin: #{name}" if !echoed_plugins.include?(name) + echoed_plugins.push(name) + rescue Exception # format error message to display + self.pluginErrorMsg(name, sname) + Kernel.exit! true + end + end + end + echoln '' if !echoed_plugins.empty? + end + #----------------------------------------------------------------------------- +end diff --git a/Data/Scripts/001_Technical/006_RPG_Sprite.rb b/Data/Scripts/001_Technical/006_RPG_Sprite.rb new file mode 100644 index 000000000..7f5e36a3e --- /dev/null +++ b/Data/Scripts/001_Technical/006_RPG_Sprite.rb @@ -0,0 +1,534 @@ +class SpriteAnimation + @@_animations = [] + @@_reference_count = {} + + def initialize(sprite) + @sprite = sprite + end + + %w[ + x y ox oy viewport flash src_rect opacity tone + ].each_with_index do |s, _i| + eval <<-__END__ + + def #{s}(*arg) + @sprite.#{s}(*arg) + end + + __END__ + end + + def self.clear + @@_animations.clear + end + + def dispose + dispose_animation + dispose_loop_animation + end + + def animation(animation, hit, height = 3) + dispose_animation + @_animation = animation + return if @_animation == nil + @_animation_hit = hit + @_animation_height = height + @_animation_duration = @_animation.frame_max + fr = 20 + if @_animation.name[/\[\s*(\d+?)\s*\]\s*$/] + fr = $~[1].to_i + end + @_animation_frame_skip = Graphics.frame_rate / fr + animation_name = @_animation.animation_name + animation_hue = @_animation.animation_hue + bitmap = pbGetAnimation(animation_name, animation_hue) + if @@_reference_count.include?(bitmap) + @@_reference_count[bitmap] += 1 + else + @@_reference_count[bitmap] = 1 + end + @_animation_sprites = [] + if @_animation.position != 3 || !@@_animations.include?(animation) + 16.times do + sprite = ::Sprite.new(self.viewport) + sprite.bitmap = bitmap + sprite.visible = false + @_animation_sprites.push(sprite) + end + unless @@_animations.include?(animation) + @@_animations.push(animation) + end + end + update_animation + end + + def loop_animation(animation) + return if animation == @_loop_animation + dispose_loop_animation + @_loop_animation = animation + return if @_loop_animation == nil + @_loop_animation_index = 0 + fr = 20 + if @_animation.name[/\[\s*(\d+?)\s*\]\s*$/] + fr = $~[1].to_i + end + @_loop_animation_frame_skip = Graphics.frame_rate / fr + animation_name = @_loop_animation.animation_name + animation_hue = @_loop_animation.animation_hue + bitmap = pbGetAnimation(animation_name, animation_hue) + if @@_reference_count.include?(bitmap) + @@_reference_count[bitmap] += 1 + else + @@_reference_count[bitmap] = 1 + end + @_loop_animation_sprites = [] + 16.times do + sprite = ::Sprite.new(self.viewport) + sprite.bitmap = bitmap + sprite.visible = false + @_loop_animation_sprites.push(sprite) + end + update_loop_animation + end + + def dispose_animation + return if @_animation_sprites == nil + sprite = @_animation_sprites[0] + if sprite != nil + @@_reference_count[sprite.bitmap] -= 1 + if @@_reference_count[sprite.bitmap] == 0 + sprite.bitmap.dispose + end + end + for sprite in @_animation_sprites + sprite.dispose + end + @_animation_sprites = nil + @_animation = nil + end + + def dispose_loop_animation + return if @_loop_animation_sprites == nil + sprite = @_loop_animation_sprites[0] + if sprite != nil + @@_reference_count[sprite.bitmap] -= 1 + if @@_reference_count[sprite.bitmap] == 0 + sprite.bitmap.dispose + end + end + for sprite in @_loop_animation_sprites + sprite.dispose + end + @_loop_animation_sprites = nil + @_loop_animation = nil + end + + def active? + return @_loop_animation_sprites != nil || @_animation_sprites != nil + end + + def effect? + return @_animation_duration > 0 + end + + def update + if @_animation != nil + quick_update = true + if Graphics.frame_count % @_animation_frame_skip == 0 + @_animation_duration -= 1 + quick_update = false + end + update_animation(quick_update) + end + if @_loop_animation != nil + quick_update = (Graphics.frame_count % @_loop_animation_frame_skip != 0) + update_loop_animation(quick_update) + if !quick_update + @_loop_animation_index += 1 + @_loop_animation_index %= @_loop_animation.frame_max + end + end + end + + def update_animation(quick_update = false) + if @_animation_duration <= 0 + dispose_animation + return + end + frame_index = @_animation.frame_max - @_animation_duration + cell_data = @_animation.frames[frame_index].cell_data + position = @_animation.position + animation_set_sprites(@_animation_sprites, cell_data, position, quick_update) + return if quick_update + for timing in @_animation.timings + next if timing.frame != frame_index + animation_process_timing(timing, @_animation_hit) + end + end + + def update_loop_animation(quick_update = false) + frame_index = @_loop_animation_index + cell_data = @_loop_animation.frames[frame_index].cell_data + position = @_loop_animation.position + animation_set_sprites(@_loop_animation_sprites, cell_data, position, quick_update) + return if quick_update + for timing in @_loop_animation.timings + next if timing.frame != frame_index + animation_process_timing(timing, true) + end + end + + def animation_set_sprites(sprites, cell_data, position, quick_update = false) + sprite_x = 320 + sprite_y = 240 + if position == 3 + if self.viewport != nil + sprite_x = self.viewport.rect.width / 2 + sprite_y = self.viewport.rect.height - 160 + end + else + sprite_x = self.x - self.ox + self.src_rect.width / 2 + sprite_y = self.y - self.oy + sprite_y += self.src_rect.height / 2 if position == 1 + sprite_y += self.src_rect.height if position == 2 + end + for i in 0..15 + sprite = sprites[i] + pattern = cell_data[i, 0] + if sprite == nil || pattern == nil || pattern == -1 + sprite.visible = false if sprite != nil + next + end + sprite.x = sprite_x + cell_data[i, 1] + sprite.y = sprite_y + cell_data[i, 2] + next if quick_update + sprite.visible = true + sprite.src_rect.set(pattern % 5 * 192, pattern / 5 * 192, 192, 192) + case @_animation_height + when 0 then sprite.z = 1 + when 1 then sprite.z = sprite.y+32+15 + when 2 then sprite.z = sprite.y+32+32+17 + else sprite.z = 2000 + end + sprite.ox = 96 + sprite.oy = 96 + sprite.zoom_x = cell_data[i, 3] / 100.0 + sprite.zoom_y = cell_data[i, 3] / 100.0 + sprite.angle = cell_data[i, 4] + sprite.mirror = (cell_data[i, 5] == 1) + sprite.tone = self.tone + sprite.opacity = cell_data[i, 6] * self.opacity / 255.0 + sprite.blend_type = cell_data[i, 7] + end + end + + def animation_process_timing(timing, hit) + if timing.condition == 0 || + (timing.condition == 1 && hit == true) || + (timing.condition == 2 && hit == false) + if timing.se.name != "" + se = timing.se + pbSEPlay(se) + end + case timing.flash_scope + when 1 + self.flash(timing.flash_color, timing.flash_duration * 2) + when 2 + if self.viewport != nil + self.viewport.flash(timing.flash_color, timing.flash_duration * 2) + end + when 3 + self.flash(nil, timing.flash_duration * 2) + end + end + end + + def x=(x) + sx = x - self.x + return if sx == 0 + if @_animation_sprites != nil + for i in 0..15 + @_animation_sprites[i].x += sx + end + end + if @_loop_animation_sprites != nil + for i in 0..15 + @_loop_animation_sprites[i].x += sx + end + end + end + + def y=(y) + sy = y - self.y + return if sy == 0 + if @_animation_sprites != nil + for i in 0..15 + @_animation_sprites[i].y += sy + end + end + if @_loop_animation_sprites != nil + for i in 0..15 + @_loop_animation_sprites[i].y += sy + end + end + end +end + + + +module RPG + class Sprite < ::Sprite + def initialize(viewport = nil) + super(viewport) + @_whiten_duration = 0 + @_appear_duration = 0 + @_escape_duration = 0 + @_collapse_duration = 0 + @_damage_duration = 0 + @_animation_duration = 0 + @_blink = false + @animations = [] + @loopAnimations = [] + end + + def dispose + dispose_damage + dispose_animation + dispose_loop_animation + super + end + + def whiten + self.blend_type = 0 + self.color.set(255, 255, 255, 128) + self.opacity = 255 + @_whiten_duration = 16 + @_appear_duration = 0 + @_escape_duration = 0 + @_collapse_duration = 0 + end + + def appear + self.blend_type = 0 + self.color.set(0, 0, 0, 0) + self.opacity = 0 + @_appear_duration = 16 + @_whiten_duration = 0 + @_escape_duration = 0 + @_collapse_duration = 0 + end + + def escape + self.blend_type = 0 + self.color.set(0, 0, 0, 0) + self.opacity = 255 + @_escape_duration = 32 + @_whiten_duration = 0 + @_appear_duration = 0 + @_collapse_duration = 0 + end + + def collapse + self.blend_type = 1 + self.color.set(255, 64, 64, 255) + self.opacity = 255 + @_collapse_duration = 48 + @_whiten_duration = 0 + @_appear_duration = 0 + @_escape_duration = 0 + end + + def damage(value, critical) + dispose_damage + damage_string = (value.is_a?(Numeric)) ? value.abs.to_s : value.to_s + bitmap = Bitmap.new(160, 48) + bitmap.font.name = "Arial Black" + bitmap.font.size = 32 + bitmap.font.color.set(0, 0, 0) + bitmap.draw_text(-1, 12-1, 160, 36, damage_string, 1) + bitmap.draw_text(+1, 12-1, 160, 36, damage_string, 1) + bitmap.draw_text(-1, 12+1, 160, 36, damage_string, 1) + bitmap.draw_text(+1, 12+1, 160, 36, damage_string, 1) + if value.is_a?(Numeric) && value < 0 + bitmap.font.color.set(176, 255, 144) + else + bitmap.font.color.set(255, 255, 255) + end + bitmap.draw_text(0, 12, 160, 36, damage_string, 1) + if critical + bitmap.font.size = 20 + bitmap.font.color.set(0, 0, 0) + bitmap.draw_text(-1, -1, 160, 20, "CRITICAL", 1) + bitmap.draw_text(+1, -1, 160, 20, "CRITICAL", 1) + bitmap.draw_text(-1, +1, 160, 20, "CRITICAL", 1) + bitmap.draw_text(+1, +1, 160, 20, "CRITICAL", 1) + bitmap.font.color.set(255, 255, 255) + bitmap.draw_text(0, 0, 160, 20, "CRITICAL", 1) + end + @_damage_sprite = ::Sprite.new(self.viewport) + @_damage_sprite.bitmap = bitmap + @_damage_sprite.ox = 80 + @_damage_sprite.oy = 20 + @_damage_sprite.x = self.x + @_damage_sprite.y = self.y - self.oy / 2 + @_damage_sprite.z = 3000 + @_damage_duration = 40 + end + + def pushAnimation(array, anim) + for i in 0...array.length + next if array[i] && array[i].active? + array[i] = anim + return + end + array.push(anim) + end + + def animation(animation, hit, height = 3) + anim = SpriteAnimation.new(self) + anim.animation(animation,hit,height) + pushAnimation(@animations,anim) + end + + def loop_animation(animation) + anim = SpriteAnimation.new(self) + anim.loop_animation(animation) + pushAnimation(@loopAnimations,anim) + end + + def dispose_damage + return if @_damage_sprite == nil + @_damage_sprite.bitmap.dispose + @_damage_sprite.dispose + @_damage_sprite = nil + @_damage_duration = 0 + end + + def dispose_animation + for a in @animations + a.dispose_animation if a + end + @animations.clear + end + + def dispose_loop_animation + for a in @loopAnimations + a.dispose_loop_animation if a + end + @loopAnimations.clear + end + + def blink_on + return if @_blink + @_blink = true + @_blink_count = 0 + end + + def blink_off + return unless @_blink + @_blink = false + self.color.set(0, 0, 0, 0) + end + + def blink? + return @_blink + end + + def effect? + return true if @_whiten_duration > 0 + return true if @_appear_duration > 0 + return true if @_escape_duration > 0 + return true if @_collapse_duration > 0 + return true if @_damage_duration > 0 + for a in @animations + return true if a.effect? + end + return false + end + + def update + super + if @_whiten_duration > 0 + @_whiten_duration -= 1 + self.color.alpha = 128 - (16 - @_whiten_duration) * 10 + end + if @_appear_duration > 0 + @_appear_duration -= 1 + self.opacity = (16 - @_appear_duration) * 16 + end + if @_escape_duration > 0 + @_escape_duration -= 1 + self.opacity = 256 - (32 - @_escape_duration) * 10 + end + if @_collapse_duration > 0 + @_collapse_duration -= 1 + self.opacity = 256 - (48 - @_collapse_duration) * 6 + end + if @_damage_duration > 0 + @_damage_duration -= 1 + case @_damage_duration + when 38..39 + @_damage_sprite.y -= 4 + when 36..37 + @_damage_sprite.y -= 2 + when 34..35 + @_damage_sprite.y += 2 + when 28..33 + @_damage_sprite.y += 4 + end + @_damage_sprite.opacity = 256 - (12 - @_damage_duration) * 32 + if @_damage_duration == 0 + dispose_damage + end + end + for a in @animations + a.update + end + for a in @loopAnimations + a.update + end + if @_blink + @_blink_count = (@_blink_count + 1) % 32 + if @_blink_count < 16 + alpha = (16 - @_blink_count) * 6 + else + alpha = (@_blink_count - 16) * 6 + end + self.color.set(255, 255, 255, alpha) + end + SpriteAnimation.clear + end + + def update_animation + for a in @animations + a.update_animation if a && a.active? + end + end + + def update_loop_animation + for a in @loopAnimations + a.update_loop_animation if a && a.active? + end + end + + def x=(x) + for a in @animations + a.x = x if a + end + for a in @loopAnimations + a.x = x if a + end + super + end + + def y=(y) + for a in @animations + a.y = y if a + end + for a in @loopAnimations + a.y = y if a + end + super + end + end +end diff --git a/Data/Scripts/002_BattleSettings.rb b/Data/Scripts/002_BattleSettings.rb new file mode 100644 index 000000000..a50ce0886 --- /dev/null +++ b/Data/Scripts/002_BattleSettings.rb @@ -0,0 +1,78 @@ +module Settings + # Whether a move's physical/special category depends on the move itself as in + # newer Gens (true), or on its type as in older Gens (false). + MOVE_CATEGORY_PER_MOVE = (MECHANICS_GENERATION >= 4) + # Whether turn order is recalculated after a Pokémon Mega Evolves. + RECALCULATE_TURN_ORDER_AFTER_MEGA_EVOLUTION = (MECHANICS_GENERATION >= 7) + # Whether turn order is recalculated after a Pokémon's Speed stat changes. + RECALCULATE_TURN_ORDER_AFTER_SPEED_CHANGES = (MECHANICS_GENERATION >= 8) + # Whether critical hits do 1.5x damage and have 4 stages (true), or they do 2x + # damage and have 5 stages as in Gen 5 (false). Also determines whether + # critical hit rate can be copied by Transform/Psych Up. + NEW_CRITICAL_HIT_RATE_MECHANICS = (MECHANICS_GENERATION >= 5) + # Whether several effects apply relating to a Pokémon's type: + # * Electric-type immunity to paralysis + # * Ghost-type immunity to being trapped + # * Grass-type immunity to powder moves and Effect Spore + # * Poison-type Pokémon can't miss when using Toxic + MORE_TYPE_EFFECTS = (MECHANICS_GENERATION >= 5) + # Whether weather caused by an ability lasts 5 rounds (true) or forever (false). + FIXED_DURATION_WEATHER_FROM_ABILITY = (MECHANICS_GENERATION >= 6) + + #============================================================================= + + # Whether X items (X Attack, etc.) raise their stat by 2 stages (true) or 1 + # (false). + X_STAT_ITEMS_RAISE_BY_TWO_STAGES = (MECHANICS_GENERATION >= 7) + # Whether some Poké Balls have catch rate multipliers from Gen 7 (true) or + # from earlier generations (false). + NEW_POKE_BALL_CATCH_RATES = (MECHANICS_GENERATION >= 7) + # Whether Soul Dew powers up Psychic and Dragon-type moves by 20% (true) or + # raises the holder's Special Attack and Special Defense by 50% (false). + SOUL_DEW_POWERS_UP_TYPES = (MECHANICS_GENERATION >= 7) + + #============================================================================= + + # The minimum number of badges required to boost each stat of a player's + # Pokémon by 1.1x, in battle only. + NUM_BADGES_BOOST_ATTACK = (MECHANICS_GENERATION >= 4) ? 999 : 1 + NUM_BADGES_BOOST_DEFENSE = (MECHANICS_GENERATION >= 4) ? 999 : 5 + NUM_BADGES_BOOST_SPATK = (MECHANICS_GENERATION >= 4) ? 999 : 7 + NUM_BADGES_BOOST_SPDEF = (MECHANICS_GENERATION >= 4) ? 999 : 7 + NUM_BADGES_BOOST_SPEED = (MECHANICS_GENERATION >= 4) ? 999 : 3 + + #============================================================================= + + # An array of items which act as Mega Rings for the player (NPCs don't need a + # Mega Ring item, just a Mega Stone held by their Pokémon). + MEGA_RINGS = [:MEGARING, :MEGABRACELET, :MEGACUFF, :MEGACHARM] + # The Game Switch which, while ON, prevents all Pokémon in battle from Mega + # Evolving even if they otherwise could. + NO_MEGA_EVOLUTION = 34 + + #============================================================================= + + # Whether the Exp gained from beating a Pokémon should be scaled depending on + # the gainer's level. + SCALED_EXP_FORMULA = (MECHANICS_GENERATION == 5 || MECHANICS_GENERATION >= 7) + # Whether the Exp gained from beating a Pokémon should be divided equally + # between each participant (true), or whether each participant should gain + # that much Exp (false). This also applies to Exp gained via the Exp Share + # (held item version) being distributed to all Exp Share holders. + SPLIT_EXP_BETWEEN_GAINERS = (MECHANICS_GENERATION <= 5) + # Whether the critical capture mechanic applies. Note that its calculation is + # based on a total of 600+ species (i.e. that many species need to be caught + # to provide the greatest critical capture chance of 2.5x), and there may be + # fewer species in your game. + ENABLE_CRITICAL_CAPTURES = false#(MECHANICS_GENERATION >= 5) + # Whether Pokémon gain Exp for capturing a Pokémon. + GAIN_EXP_FOR_CAPTURE = (MECHANICS_GENERATION >= 5) + # The Game Switch which, whie ON, prevents the player from losing money if + # they lose a battle (they can still gain money from trainers for winning). + NO_MONEY_LOSS = 33 + # Whether party Pokémon check whether they can evolve after all battles + # regardless of the outcome (true), or only after battles the player won (false). + CHECK_EVOLUTION_AFTER_ALL_BATTLES = (MECHANICS_GENERATION >= 6) + # Whether fainted Pokémon can try to evolve after a battle. + CHECK_EVOLUTION_FOR_FAINTED_POKEMON = true +end diff --git a/Data/Scripts/002_Save data/001_SaveData.rb b/Data/Scripts/002_Save data/001_SaveData.rb new file mode 100644 index 000000000..6dfcb4390 --- /dev/null +++ b/Data/Scripts/002_Save data/001_SaveData.rb @@ -0,0 +1,96 @@ +# The SaveData module is used to manipulate save data. It contains the {Value}s +# that make up the save data and {Conversion}s for resolving incompatibilities +# between Essentials and game versions. +# @see SaveData.register +# @see SaveData.register_conversion +module SaveData + # Contains the file path of the save file. + FILE_PATH = if File.directory?(System.data_directory) + System.data_directory + '/Game.rxdata' + else + './Game.rxdata' + end + + # @return [Boolean] whether the save file exists + def self.exists? + return File.file?(FILE_PATH) + end + + # Fetches the save data from the given file. + # Returns an Array in the case of a pre-v19 save file. + # @param file_path [String] path of the file to load from + # @return [Hash, Array] loaded save data + # @raise [IOError, SystemCallError] if file opening fails + def self.get_data_from_file(file_path) validate file_path => String + save_data = nil + File.open(file_path) do |file| + data = Marshal.load(file) + if data.is_a?(Hash) + save_data = data + next + end + save_data = [data] + save_data << Marshal.load(file) until file.eof? + end + return save_data + end + + # Fetches save data from the given file. If it needed converting, resaves it. + # @param file_path [String] path of the file to read from + # @return [Hash] save data in Hash format + # @raise (see .get_data_from_file) + def self.read_from_file(file_path) + validate file_path => String + save_data = get_data_from_file(file_path) + save_data = to_hash_format(save_data) if save_data.is_a?(Array) + if !save_data.empty? && run_conversions(save_data) + File.open(file_path, 'wb') { |file| Marshal.dump(save_data, file) } + end + return save_data + end + + # Compiles the save data and saves a marshaled version of it into + # the given file. + # @param file_path [String] path of the file to save into + # @raise [InvalidValueError] if an invalid value is being saved + def self.save_to_file(file_path) + validate file_path => String + save_data = self.compile_save_hash + File.open(file_path, 'wb') { |file| Marshal.dump(save_data, file) } + end + + # Deletes the save file (and a possible .bak backup file if one exists) + # @raise [Error::ENOENT] + def self.delete_file + File.delete(FILE_PATH) + File.delete(FILE_PATH + '.bak') if File.file?(FILE_PATH + '.bak') + end + + # Converts the pre-v19 format data to the new format. + # @param old_format [Array] pre-v19 format save data + # @return [Hash] save data in new format + def self.to_hash_format(old_format) + validate old_format => Array + hash = {} + @values.each do |value| + data = value.get_from_old_format(old_format) + hash[value.id] = data unless data.nil? + end + return hash + end + + # Moves a save file from the old Saved Games folder to the new + # location specified by {FILE_PATH}. Does nothing if a save file + # already exists in {FILE_PATH}. + def self.move_old_windows_save + return if File.file?(FILE_PATH) + game_title = System.game_title.gsub(/[^\w ]/, '_') + home = ENV['HOME'] || ENV['HOMEPATH'] + return if home.nil? + old_location = File.join(home, 'Saved Games', game_title) + return unless File.directory?(old_location) + old_file = File.join(old_location, 'Game.rxdata') + return unless File.file?(old_file) + File.move(old_file, FILE_PATH) + end +end diff --git a/Data/Scripts/002_Save data/002_SaveData_Value.rb b/Data/Scripts/002_Save data/002_SaveData_Value.rb new file mode 100644 index 000000000..7e5bdf9da --- /dev/null +++ b/Data/Scripts/002_Save data/002_SaveData_Value.rb @@ -0,0 +1,269 @@ +module SaveData + # Contains Value objects for each save element. + # Populated during runtime by SaveData.register calls. + # @type [Array] + @values = [] + + # An error raised if an invalid save value is being saved or loaded. + class InvalidValueError < RuntimeError; end + + #============================================================================= + # Represents a single value in save data. + # New values are added using {SaveData.register}. + class Value + # @return [Symbol] the value id + attr_reader :id + + # @param id [Symbol] value id + def initialize(id, &block) + validate id => Symbol, block => Proc + @id = id + @loaded = false + @load_in_bootup = false + instance_eval(&block) + raise "No save_value defined for save value #{id.inspect}" if @save_proc.nil? + raise "No load_value defined for save value #{id.inspect}" if @load_proc.nil? + end + + # @param value [Object] value to check + # @return [Boolean] whether the given value is valid + def valid?(value) + return true if @ensured_class.nil? + return value.is_a?(Object.const_get(@ensured_class)) + end + + # Calls the value's load proc with the given argument passed into it. + # @param value [Object] load proc argument + # @raise [InvalidValueError] if an invalid value is being loaded + def load(value) + validate_value(value) + @load_proc.call(value) + @loaded = true + end + + # Calls the value's save proc and returns its value. + # @return [Object] save proc value + # @raise [InvalidValueError] if an invalid value is being saved + def save + value = @save_proc.call + validate_value(value) + return value + end + + # @return [Boolean] whether the value has a new game value proc defined + def has_new_game_proc? + return @new_game_value_proc.is_a?(Proc) + end + + # Calls the save value's load proc with the value fetched + # from the defined new game value proc. + # @raise (see #load) + def load_new_game_value + unless self.has_new_game_proc? + raise "Save value #{@id.inspect} has no new_game_value defined" + end + self.load(@new_game_value_proc.call) + end + + # @return [Boolean] whether the value should be loaded during bootup + def load_in_bootup? + return @load_in_bootup + end + + # @return [Boolean] whether the value has been loaded + def loaded? + return @loaded + end + + # Marks value as unloaded. + def mark_as_unloaded + @loaded = false + end + + # Uses the {#from_old_format} proc to select the correct data from + # +old_format+ and return it. + # Returns nil if the proc is undefined. + # @param old_format [Array] old format to load value from + # @return [Object] data from the old format + def get_from_old_format(old_format) + return nil if @old_format_get_proc.nil? + return @old_format_get_proc.call(old_format) + end + + private + + # Raises an {InvalidValueError} if the given value is invalid. + # @param value [Object] value to check + # @raise [InvalidValueError] if the value is invalid + def validate_value(value) + return if self.valid?(value) + raise InvalidValueError, "Save value #{@id.inspect} is not a #{@ensured_class} (#{value.class.name} given)" + end + + # @!group Configuration + + # If present, ensures that the value is of the given class. + # @param class_name [Symbol] class to enforce + # @see SaveData.register + def ensure_class(class_name) + validate class_name => Symbol + @ensured_class = class_name + end + + # Defines how the loaded value is placed into a global variable. + # Requires a block with the loaded value as its parameter. + # @see SaveData.register + def load_value(&block) + raise ArgumentError, 'No block given to load_value' unless block_given? + @load_proc = block + end + + # Defines what is saved into save data. Requires a block. + # @see SaveData.register + def save_value(&block) + raise ArgumentError, 'No block given to save_value' unless block_given? + @save_proc = block + end + + # If present, defines what the value is set to at the start of a new game. + # @see SaveData.register + def new_game_value(&block) + raise ArgumentError, 'No block given to new_game_value' unless block_given? + @new_game_value_proc = block + end + + # If present, sets the value to be loaded during bootup. + # @see SaveData.register + def load_in_bootup + @load_in_bootup = true + end + + # If present, defines how the value should be fetched from the pre-v19 + # save format. Requires a block with the old format array as its parameter. + # @see SaveData.register + def from_old_format(&block) + raise ArgumentError, 'No block given to from_old_format' unless block_given? + @old_format_get_proc = block + end + + # @!endgroup + end + + #============================================================================= + # Registers a {Value} to be saved into save data. + # Takes a block which defines the value's saving ({Value#save_value}) + # and loading ({Value#load_value}) procedures. + # + # It is also possible to provide a proc for fetching the value + # from the pre-v19 format ({Value#from_old_format}), define + # a value to be set upon starting a new game with {Value#new_game_value} + # and ensure that the saved and loaded value is of the correct + # class with {Value#ensure_class}. + # + # Values can be registered to be loaded on bootup with + # {Value#load_in_bootup}. If a new_game_value proc is defined, it + # will be called when the game is launched for the first time, + # or if the save data does not contain the value in question. + # + # @example Registering a new value + # SaveData.register(:foo) do + # ensure_class :Foo + # save_value { $foo } + # load_value { |value| $foo = value } + # new_game_value { Foo.new } + # from_old_format { |old_format| old_format[16] if old_format[16].is_a?(Foo) } + # end + # @example Registering a value to be loaded on bootup + # SaveData.register(:bar) do + # load_in_bootup + # save_value { $bar } + # load_value { |value| $bar = value } + # new_game_value { Bar.new } + # end + # @param id [Symbol] value id + # @yieldself [Value] + def self.register(id, &block) + validate id => Symbol + unless block_given? + raise ArgumentError, 'No block given to SaveData.register' + end + @values << Value.new(id, &block) + end + + # @param save_data [Hash] save data to validate + # @return [Boolean] whether the given save data is valid + def self.valid?(save_data) + validate save_data => Hash + return @values.all? { |value| value.valid?(save_data[value.id]) } + end + + # Loads values from the given save data. + # An optional condition can be passed. + # @param save_data [Hash] save data to load from + # @param condition_block [Proc] optional condition + # @api private + def self.load_values(save_data, &condition_block) + @values.each do |value| + next if block_given? && !condition_block.call(value) + if save_data.has_key?(value.id) + value.load(save_data[value.id]) + elsif value.has_new_game_proc? + value.load_new_game_value + end + end + end + + # Loads the values from the given save data by + # calling each {Value} object's {Value#load_value} proc. + # Values that are already loaded are skipped. + # If a value does not exist in the save data and has + # a {Value#new_game_value} proc defined, that value + # is loaded instead. + # @param save_data [Hash] save data to load + # @raise [InvalidValueError] if an invalid value is being loaded + def self.load_all_values(save_data) + validate save_data => Hash + load_values(save_data) { |value| !value.loaded? } + end + + # Marks all values that aren't loaded on bootup as unloaded. + def self.mark_values_as_unloaded + @values.each do |value| + value.mark_as_unloaded unless value.load_in_bootup? + end + end + + # Loads each value from the given save data that has + # been set to be loaded during bootup. Done when a save file exists. + # @param save_data [Hash] save data to load + # @raise [InvalidValueError] if an invalid value is being loaded + def self.load_bootup_values(save_data) + validate save_data => Hash + load_values(save_data) { |value| !value.loaded? && value.load_in_bootup? } + end + + # Goes through each value with {Value#load_in_bootup} enabled and loads their + # new game value, if one is defined. Done when no save file exists. + def self.initialize_bootup_values + @values.each do |value| + next unless value.load_in_bootup? + value.load_new_game_value if value.has_new_game_proc? && !value.loaded? + end + end + + # Loads each {Value}'s new game value, if one is defined. Done when starting a + # new game. + def self.load_new_game_values + @values.each do |value| + value.load_new_game_value if value.has_new_game_proc? && !value.loaded? + end + end + + # @return [Hash{Symbol => Object}] a hash representation of the save data + # @raise [InvalidValueError] if an invalid value is being saved + def self.compile_save_hash + save_data = {} + @values.each { |value| save_data[value.id] = value.save } + return save_data + end +end diff --git a/Data/Scripts/002_Save data/003_SaveData_Conversion.rb b/Data/Scripts/002_Save data/003_SaveData_Conversion.rb new file mode 100644 index 000000000..f48bc35ea --- /dev/null +++ b/Data/Scripts/002_Save data/003_SaveData_Conversion.rb @@ -0,0 +1,221 @@ +module SaveData + # Contains Conversion objects for each defined conversion: + # { + # :essentials => { + # '19' => [, ...], + # '19.1' => [, ...], + # ... + # }, + # :game => { + # '1.1.0' => [, ...], + # '1.2.0' => [, ...], + # ... + # } + # } + # Populated during runtime by SaveData.register_conversion calls. + @conversions = { + essentials: {}, + game: {} + } + + #============================================================================= + # Represents a conversion made to save data. + # New conversions are added using {SaveData.register_conversion}. + class Conversion + # @return [Symbol] conversion ID + attr_reader :id + # @return [String] conversion title + attr_reader :title + # @return [Symbol] trigger type of the conversion (+:essentials+ or +:game+) + attr_reader :trigger_type + # @return [String] trigger version of the conversion + attr_reader :version + + # @param id [String] conversion ID + def initialize(id, &block) + @id = id + @value_procs = {} + @all_proc = nil + @title = "Running conversion #{@id}" + @trigger_type = nil + @version = nil + instance_eval(&block) + if @trigger_type.nil? || @version.nil? + raise "Conversion #{@id} is missing a condition" + end + end + + # Returns whether the conversion should be run with the given version. + # @param version [String] version to check + # @return [Boolean] whether the conversion should be run + def should_run?(version) + return PluginManager.compare_versions(version, @version) < 0 + end + + # Runs the conversion on the given save data. + # @param save_data [Hash] + def run(save_data) + @value_procs.each do |value_id, proc| + unless save_data.has_key?(value_id) + raise "Save data does not have value #{value_id.inspect}" + end + proc.call(save_data[value_id]) + end + @all_proc.call(save_data) if @all_proc.is_a?(Proc) + end + + # Runs the conversion on the given object. + # @param object + # @param key [Symbol] + def run_single(object, key) + @value_procs[key].call(object) if @value_procs[key].is_a?(Proc) + end + + private + + # @!group Configuration + + # Sets the conversion's title. + # @param new_title [String] conversion title + # @note Since conversions are run before loading the player's chosen language, + # conversion titles can not be localized. + # @see SaveData.register_conversion + def display_title(new_title) + validate new_title => String + @title = new_title + end + + # Sets the conversion to trigger for save files created below + # the given Essentials version. + # @param version [Numeric, String] + # @see SaveData.register_conversion + def essentials_version(version) + validate version => [Numeric, String] + raise "Multiple conditions in conversion #{@id}" unless @version.nil? + @trigger_type = :essentials + @version = version.to_s + end + + # Sets the conversion to trigger for save files created below + # the given game version. + # @param version [Numeric, String] + # @see SaveData.register_conversion + def game_version(version) + validate version => [Numeric, String] + raise "Multiple conditions in conversion #{@id}" unless @version.nil? + @trigger_type = :game + @version = version.to_s + end + + # Defines a conversion to the given save value. + # @param value_id [Symbol] save value ID + # @see SaveData.register_conversion + def to_value(value_id, &block) + validate value_id => Symbol + raise ArgumentError, 'No block given to to_value' unless block_given? + if @value_procs[value_id].is_a?(Proc) + raise "Multiple to_value definitions in conversion #{@id} for #{value_id}" + end + @value_procs[value_id] = block + end + + # Defines a conversion to the entire save data. + # @see SaveData.register_conversion + def to_all(&block) + raise ArgumentError, 'No block given to to_all' unless block_given? + if @all_proc.is_a?(Proc) + raise "Multiple to_all definitions in conversion #{@id}" + end + @all_proc = block + end + + # @!endgroup + end + + #============================================================================= + # Registers a {Conversion} to occur for save data that meets the given criteria. + # Two types of criteria can be defined: {Conversion#essentials_version} and + # {Conversion#game_version}. The conversion is automatically run on save data + # that contains an older version number. + # + # A single value can be modified with {Conversion#to_value}. The entire save data + # is accessed with {Conversion#to_all}, and a conversion title can be specified + # with {Conversion#display_title}. + # @example Registering a new conversion + # SaveData.register_conversion(:my_conversion) do + # game_version '1.1.0' + # display_title 'Converting some stuff' + # to_value :player do |player| + # # code that modifies the :player value + # end + # to_all do |save_data| + # save_data[:new_value] = Foo.new + # end + # end + # @yield self [Conversion] + def self.register_conversion(id, &block) + validate id => Symbol + unless block_given? + raise ArgumentError, 'No block given to SaveData.register_conversion' + end + conversion = Conversion.new(id, &block) + @conversions[conversion.trigger_type][conversion.version] ||= [] + @conversions[conversion.trigger_type][conversion.version] << conversion + end + + # @param save_data [Hash] save data to get conversions for + # @return [Array] all conversions that should be run on the data + def self.get_conversions(save_data) + conversions_to_run = [] + versions = { + essentials: save_data[:essentials_version] || '18.1', + game: save_data[:game_version] || '0.0.0' + } + [:essentials, :game].each do |trigger_type| + # Ensure the versions are sorted from lowest to highest + sorted_versions = @conversions[trigger_type].keys.sort do |v1, v2| + PluginManager.compare_versions(v1, v2) + end + sorted_versions.each do |version| + @conversions[trigger_type][version].each do |conversion| + next unless conversion.should_run?(versions[trigger_type]) + conversions_to_run << conversion + end + end + end + return conversions_to_run + end + + # Runs all possible conversions on the given save data. + # Saves a backup before running conversions. + # @param save_data [Hash] save data to run conversions on + # @return [Boolean] whether conversions were run + def self.run_conversions(save_data) + validate save_data => Hash + conversions_to_run = self.get_conversions(save_data) + return false if conversions_to_run.none? + File.open(SaveData::FILE_PATH + '.bak', 'wb') { |f| Marshal.dump(save_data, f) } + echoln "Running #{conversions_to_run.length} conversions..." + conversions_to_run.each do |conversion| + echo "#{conversion.title}..." + conversion.run(save_data) + echoln ' done.' + end + echoln '' if conversions_to_run.length > 0 + save_data[:essentials_version] = Essentials::VERSION + save_data[:game_version] = Settings::GAME_VERSION + return true + end + + # Runs all possible conversions on the given object. + # @param object [Hash] object to run conversions on + # @param key [Hash] object's key in save data + # @param save_data [Hash] save data to run conversions on + def self.run_single_conversions(object, key, save_data) + validate key => Symbol + conversions_to_run = self.get_conversions(save_data) + conversions_to_run.each do |conversion| + conversion.run_single(object, key) + end + end +end diff --git a/Data/Scripts/002_Save data/004_Game_SaveValues.rb b/Data/Scripts/002_Save data/004_Game_SaveValues.rb new file mode 100644 index 000000000..18d3ab07b --- /dev/null +++ b/Data/Scripts/002_Save data/004_Game_SaveValues.rb @@ -0,0 +1,135 @@ +# Contains the save values defined in Essentials by default. + +SaveData.register(:player) do + ensure_class :Player + save_value { $Trainer } + load_value { |value| $Trainer = value } + new_game_value { + trainer_type = nil # Get the first defined trainer type as a placeholder + GameData::TrainerType.each { |t| trainer_type = t.id; break } + Player.new("Unnamed", trainer_type) + } + from_old_format { |old_format| old_format[0] } +end + +SaveData.register(:frame_count) do + ensure_class :Integer + save_value { Graphics.frame_count } + load_value { |value| Graphics.frame_count = value } + new_game_value { 0 } + from_old_format { |old_format| old_format[1] } +end + +SaveData.register(:game_system) do + load_in_bootup + ensure_class :Game_System + save_value { $game_system } + load_value { |value| $game_system = value } + new_game_value { Game_System.new } + from_old_format { |old_format| old_format[2] } +end + +SaveData.register(:pokemon_system) do + load_in_bootup + ensure_class :PokemonSystem + save_value { $PokemonSystem } + load_value { |value| $PokemonSystem = value } + new_game_value { PokemonSystem.new } + from_old_format { |old_format| old_format[3] } +end + +SaveData.register(:switches) do + ensure_class :Game_Switches + save_value { $game_switches } + load_value { |value| $game_switches = value } + new_game_value { Game_Switches.new } + from_old_format { |old_format| old_format[5] } +end + +SaveData.register(:variables) do + ensure_class :Game_Variables + save_value { $game_variables } + load_value { |value| $game_variables = value } + new_game_value { Game_Variables.new } + from_old_format { |old_format| old_format[6] } +end + +SaveData.register(:self_switches) do + ensure_class :Game_SelfSwitches + save_value { $game_self_switches } + load_value { |value| $game_self_switches = value } + new_game_value { Game_SelfSwitches.new } + from_old_format { |old_format| old_format[7] } +end + +SaveData.register(:game_screen) do + ensure_class :Game_Screen + save_value { $game_screen } + load_value { |value| $game_screen = value } + new_game_value { Game_Screen.new } + from_old_format { |old_format| old_format[8] } +end + +SaveData.register(:map_factory) do + ensure_class :PokemonMapFactory + save_value { $MapFactory } + load_value { |value| $MapFactory = value } + from_old_format { |old_format| old_format[9] } +end + +SaveData.register(:game_player) do + ensure_class :Game_Player + save_value { $game_player } + load_value { |value| $game_player = value } + new_game_value { Game_Player.new } + from_old_format { |old_format| old_format[10] } +end + +SaveData.register(:global_metadata) do + ensure_class :PokemonGlobalMetadata + save_value { $PokemonGlobal } + load_value { |value| $PokemonGlobal = value } + new_game_value { PokemonGlobalMetadata.new } + from_old_format { |old_format| old_format[11] } +end + +SaveData.register(:map_metadata) do + ensure_class :PokemonMapMetadata + save_value { $PokemonMap } + load_value { |value| $PokemonMap = value } + new_game_value { PokemonMapMetadata.new } + from_old_format { |old_format| old_format[12] } +end + +SaveData.register(:bag) do + ensure_class :PokemonBag + save_value { $PokemonBag } + load_value { |value| $PokemonBag = value } + new_game_value { PokemonBag.new } + from_old_format { |old_format| old_format[13] } +end + +SaveData.register(:storage_system) do + ensure_class :PokemonStorage + save_value { $PokemonStorage } + load_value { |value| $PokemonStorage = value } + new_game_value { PokemonStorage.new } + from_old_format { |old_format| old_format[14] } +end + +SaveData.register(:essentials_version) do + load_in_bootup + ensure_class :String + save_value { Essentials::VERSION } + load_value { |value| $SaveVersion = value } + new_game_value { Essentials::VERSION } + from_old_format { |old_format| old_format[15] } +end + +SaveData.register(:game_version) do + load_in_bootup + ensure_class :String + save_value { Settings::GAME_VERSION } + load_value { |value| $game_version = value } + new_game_value { Settings::GAME_VERSION } +end diff --git a/Data/Scripts/002_Save data/005_Game_SaveConversions.rb b/Data/Scripts/002_Save data/005_Game_SaveConversions.rb new file mode 100644 index 000000000..554864de6 --- /dev/null +++ b/Data/Scripts/002_Save data/005_Game_SaveConversions.rb @@ -0,0 +1,242 @@ +# Contains conversions defined in Essentials by default. + +SaveData.register_conversion(:v19_define_versions) do + essentials_version 19 + display_title 'Adding game version and Essentials version to save data' + to_all do |save_data| + unless save_data.has_key?(:essentials_version) + save_data[:essentials_version] = Essentials::VERSION + end + unless save_data.has_key?(:game_version) + save_data[:game_version] = Settings::GAME_VERSION + end + end +end + +SaveData.register_conversion(:v19_convert_PokemonSystem) do + essentials_version 19 + display_title 'Updating PokemonSystem class' + to_all do |save_data| + new_system = PokemonSystem.new + new_system.textspeed = save_data[:pokemon_system].textspeed || new_system.textspeed + new_system.battlescene = save_data[:pokemon_system].battlescene || new_system.battlescene + new_system.battlestyle = save_data[:pokemon_system].battlestyle || new_system.battlestyle + new_system.frame = save_data[:pokemon_system].frame || new_system.frame + new_system.textskin = save_data[:pokemon_system].textskin || new_system.textskin + new_system.screensize = save_data[:pokemon_system].screensize || new_system.screensize + new_system.language = save_data[:pokemon_system].language || new_system.language + new_system.runstyle = save_data[:pokemon_system].runstyle || new_system.runstyle + new_system.bgmvolume = save_data[:pokemon_system].bgmvolume || new_system.bgmvolume + new_system.sevolume = save_data[:pokemon_system].sevolume || new_system.sevolume + new_system.textinput = save_data[:pokemon_system].textinput || new_system.textinput + save_data[:pokemon_system] = new_system + end +end + +SaveData.register_conversion(:v19_convert_player) do + essentials_version 19 + display_title 'Converting player trainer class' + to_all do |save_data| + next if save_data[:player].is_a?(Player) + # Conversion of the party is handled in PokeBattle_Trainer.convert + save_data[:player] = PokeBattle_Trainer.convert(save_data[:player]) + end +end + +SaveData.register_conversion(:v19_move_global_data_to_player) do + essentials_version 19 + display_title 'Moving some global metadata data to player' + to_all do |save_data| + global = save_data[:global_metadata] + player = save_data[:player] + player.character_ID = global.playerID + global.playerID = nil + global.pokedexUnlocked.each_with_index do |value, i| + if value + player.pokedex.unlock(i) + else + player.pokedex.lock(i) + end + end + player.coins = global.coins + global.coins = nil + player.soot = global.sootsack + global.sootsack = nil + player.has_running_shoes = global.runningShoes + global.runningShoes = nil + player.seen_storage_creator = global.seenStorageCreator + global.seenStorageCreator = nil + player.has_snag_machine = global.snagMachine + global.snagMachine = nil + player.seen_purify_chamber = global.seenPurifyChamber + global.seenPurifyChamber = nil + end +end + +SaveData.register_conversion(:v19_convert_global_metadata) do + essentials_version 19 + display_title 'Adding encounter version variable to global metadata' + to_value :global_metadata do |global| + global.bridge ||= 0 + global.encounter_version ||= 0 + if global.pcItemStorage + global.pcItemStorage.items.each_with_index do |slot, i| + item_data = GameData::Item.try_get(slot[0]) + if item_data + slot[0] = item_data.id + else + global.pcItemStorage.items[i] = nil + end + end + global.pcItemStorage.items.compact! + end + if global.mailbox + global.mailbox.each_with_index do |mail, i| + global.mailbox[i] = PokemonMail.convert(mail) if mail + end + end + global.phoneNumbers.each do |contact| + contact[1] = GameData::TrainerType.get(contact[1]).id if contact && contact.length == 8 + end + if global.partner + global.partner[0] = GameData::TrainerType.get(global.partner[0]).id + global.partner[3].each_with_index do |pkmn, i| + global.partner[3][i] = PokeBattle_Pokemon.convert(pkmn) if pkmn + end + end + if global.daycare + global.daycare.each do |slot| + slot[0] = PokeBattle_Pokemon.convert(slot[0]) if slot && slot[0] + end + end + if global.roamPokemon + global.roamPokemon.each_with_index do |pkmn, i| + global.roamPokemon[i] = PokeBattle_Pokemon.convert(pkmn) if pkmn && pkmn != true + end + end + global.purifyChamber.sets.each do |set| + set.shadow = PokeBattle_Pokemon.convert(set.shadow) if set.shadow + set.list.each_with_index do |pkmn, i| + set.list[i] = PokeBattle_Pokemon.convert(pkmn) if pkmn + end + end + if global.hallOfFame + global.hallOfFame.each do |team| + next if !team + team.each_with_index do |pkmn, i| + team[i] = PokeBattle_Pokemon.convert(pkmn) if pkmn + end + end + end + if global.triads + global.triads.items.each do |card| + card[0] = GameData::Species.get(card[0]).id if card && card[0] && card[0] != 0 + end + end + end +end + +SaveData.register_conversion(:v19_1_fix_phone_contacts) do + essentials_version 19.1 + display_title 'Fixing phone contacts data' + to_value :global_metadata do |global| + global.phoneNumbers.each do |contact| + contact[1] = GameData::TrainerType.get(contact[1]).id if contact && contact.length == 8 + end + end +end + +SaveData.register_conversion(:v19_convert_bag) do + essentials_version 19 + display_title 'Converting item IDs in Bag' + to_value :bag do |bag| + bag.instance_eval do + for pocket in self.pockets + pocket.each_with_index do |item, i| + next if !item || !item[0] || item[0] == 0 + item_data = GameData::Item.try_get(item[0]) + if item_data + item[0] = item_data.id + else + pocket[i] = nil + end + end + pocket.compact! + end + self.registeredIndex # Just to ensure this data exists + self.registeredItems.each_with_index do |item, i| + next if !item + if item == 0 + self.registeredItems[i] = nil + else + item_data = GameData::Item.try_get(item) + if item_data + self.registeredItems[i] = item_data.id + else + self.registeredItems[i] = nil + end + end + end + self.registeredItems.compact! + end # bag.instance_eval + end # to_value +end + +SaveData.register_conversion(:v19_convert_game_variables) do + essentials_version 19 + display_title 'Converting classes of things in Game Variables' + to_all do |save_data| + variables = save_data[:variables] + for i in 0..5000 + value = variables[i] + next if value.nil? + if value.is_a?(Array) + value.each_with_index do |value2, j| + if value2.is_a?(PokeBattle_Pokemon) + value[j] = PokeBattle_Pokemon.convert(value2) + end + end + elsif value.is_a?(PokeBattle_Pokemon) + variables[i] = PokeBattle_Pokemon.convert(value) + elsif value.is_a?(PokemonBag) + SaveData.run_single_conversions(value, :bag, save_data) + end + end + end +end + +SaveData.register_conversion(:v19_convert_storage) do + essentials_version 19 + display_title 'Converting classes of Pokémon in storage' + to_value :storage_system do |storage| + storage.instance_eval do + for box in 0...self.maxBoxes + for i in 0...self.maxPokemon(box) + self[box, i] = PokeBattle_Pokemon.convert(self[box, i]) if self[box, i] + end + end + self.unlockedWallpapers # Just to ensure this data exists + end # storage.instance_eval + end # to_value +end + +SaveData.register_conversion(:v19_convert_game_player) do + essentials_version 19 + display_title 'Converting game player character' + to_value :game_player do |game_player| + game_player.width = 1 + game_player.height = 1 + game_player.sprite_size = [Game_Map::TILE_WIDTH, Game_Map::TILE_HEIGHT] + game_player.pattern_surf ||= 0 + game_player.lock_pattern ||= false + game_player.move_speed = game_player.move_speed + end +end + +SaveData.register_conversion(:v19_convert_game_screen) do + essentials_version 19 + display_title 'Converting game screen' + to_value :game_screen do |game_screen| + game_screen.weather(game_screen.weather_type, game_screen.weather_max, 0) + end +end diff --git a/Data/Scripts/003_Game processing/001_StartGame.rb b/Data/Scripts/003_Game processing/001_StartGame.rb new file mode 100644 index 000000000..4eab8ac50 --- /dev/null +++ b/Data/Scripts/003_Game processing/001_StartGame.rb @@ -0,0 +1,259 @@ +# The Game module contains methods for saving and loading the game. +module Game + # Initializes various global variables and loads the game data. + + + def self.initialize + $PokemonTemp = PokemonTemp.new + $game_temp = Game_Temp.new + $game_system = Game_System.new + $data_animations = load_data('Data/Animations.rxdata') + $data_tilesets = load_data('Data/Tilesets.rxdata') + $data_common_events = load_data('Data/CommonEvents.rxdata') + $data_system = load_data('Data/System.rxdata') + pbLoadBattleAnimations + load_sprites_list_caches() + $updated_spritesheets = load_updated_spritesheets() + GameData.load_all + map_file = format('Data/Map%03d.rxdata', $data_system.start_map_id) + if $data_system.start_map_id == 0 || !pbRgssExists?(map_file) + raise _INTL('No starting position was set in the map editor.') + end + end + + def self.load_updated_spritesheets + updated_spritesheets_file = Settings::UPDATED_SPRITESHEETS_CACHE + updated_spritesheets = [] + if File.exist?(updated_spritesheets_file) + File.open(updated_spritesheets_file, "r") do |file| + file.each_line { |line| updated_spritesheets << line.chomp } + end + end + return updated_spritesheets + end + + def self.load_sprites_list_caches() + self.load_custom_sprites_list_cache() if File.exists?(Settings::CUSTOM_SPRITES_FILE_PATH) + self.load_base_sprites_list_cache() if File.exists?(Settings::BASE_SPRITES_FILE_PATH) + end + + def self.load_custom_sprites_list_cache() + return if !$game_temp.custom_sprites_list.keys.empty? #only load once at loadup + echoln "loading custom sprites cache" + sprite_index = {} + File.foreach(Settings::CUSTOM_SPRITES_FILE_PATH) do |line| + filename = line.strip + next unless filename =~ /^(\d+)\.(\d+)([a-zA-Z]*)\.png$/ # Regex: Captures the numbers and any trailing letters + + # Match groups + head_number = $1.to_i # Head (e.g., "1" in "1.2.png") + body_number = $2.to_i # Body (e.g., "2" in "1.2.png") + letters = $3 # Letters after the second number (e.g., "a", "b", etc.) + + key = "B#{body_number}H#{head_number}".to_sym + sprite_index[key] ||= [] + if letters.empty? + sprite_index[key] << "" + else + sprite_index[key] << letters + end + end + $game_temp.custom_sprites_list = sprite_index + echoln "custom sprites loaded" + end + + # + # {1 => ["","a","b"] + #etc. + # + def self.load_base_sprites_list_cache() + return if !$game_temp.base_sprites_list.keys.empty? #only load once at loadup + echoln "loading base sprites cache" + sprite_index = {} + File.foreach(Settings::BASE_SPRITES_FILE_PATH) do |line| + filename = line.strip + next unless filename =~ /^(\d+)([a-zA-Z]*)\.png$/ # Regex: Captures the numbers and any trailing letters + + # Match groups + dex_number = $1.to_i # Head (e.g., "1" in "1.2.png") + letters = $2 # Letters after the second number (e.g., "a", "b", etc.) + + key = dex_number + sprite_index[key] ||= [] + if letters.empty? + sprite_index[key] << "" + else + sprite_index[key] << letters + end + end + $game_temp.base_sprites_list = sprite_index + echoln "custom sprites loaded" + end + + # + # {:B10H10 => ["","a","b"] + #etc. + # + def self.set_up_system + SaveData.move_old_windows_save if System.platform[/Windows/] + save_data = (SaveData.exists?) ? SaveData.read_from_file(SaveData::FILE_PATH) : {} + if save_data.empty? + SaveData.initialize_bootup_values + else + SaveData.load_bootup_values(save_data) + end + # Set resize factor + pbSetResizeFactor([$PokemonSystem.screensize, 4].min) + # Set language (and choose language if there is no save file) + if Settings::LANGUAGES.length >= 2 + $PokemonSystem.language = pbChooseLanguage if save_data.empty? + pbLoadMessages('Data/' + Settings::LANGUAGES[$PokemonSystem.language][1]) + end + end + + #For new game plus - resets everything in boxes/party to level 5 and 1st stage + def self.ngp_clean_pc_data(old_storage, old_party) + new_storage = old_storage + for pokemon in old_party + new_storage.pbStoreCaught(pokemon) + end + + for box in new_storage.boxes + for pokemon in box.pokemon + if pokemon != nil + if !pokemon.egg? + pokemon.exp_when_fused_head=nil + pokemon.exp_when_fused_body=nil + pokemon.exp_gained_since_fused=nil + pokemon.level = 5 + + echoln pokemon.owner.id + pokemon.owner.id = $Trainer.id + pokemon.ot=$Trainer.name + pokemon.obtain_method = 0 + pokemon.species = GameData::Species.get(pokemon.species).get_baby_species(false) + $Trainer.pokedex.set_seen(pokemon.species) + $Trainer.pokedex.set_owned(pokemon.species) + pokemon.reset_moves + pokemon.calc_stats + + end + end + end + end + return new_storage + end + + #For new game plus - removes key items + def self.ngp_clean_item_data(old_bag) + new_storage = old_bag + new_storage.clear + + for pocket in old_bag.pockets + for bagElement in pocket + item_id = bagElement[0] + item_qt = bagElement[1] + item = GameData::Item.get(item_id) + if !item.is_key_item? && !item.is_HM? + new_storage.pbStoreItem(item, 1) + end + end + end + return new_storage + end + + # Called when starting a new game. Initializes global variables + # and transfers the player into the map scene. + def self.start_new(ngp_bag = nil, ngp_storage = nil, ngp_trainer = nil) + + if $game_map && $game_map.events + $game_map.events.each_value { |event| event.clear_starting } + end + $game_temp.common_event_id = 0 if $game_temp + $PokemonTemp.begunNewGame = true + $scene = Scene_Map.new + SaveData.load_new_game_values + $MapFactory = PokemonMapFactory.new($data_system.start_map_id) + $game_player.moveto($data_system.start_x, $data_system.start_y) + $game_player.refresh + $PokemonEncounters = PokemonEncounters.new + $PokemonEncounters.setup($game_map.map_id) + $game_map.autoplay + $game_map.update + # + # if ngp_bag != nil + # $PokemonBag = ngp_clean_item_data(ngp_bag) + # end + if ngp_storage != nil + $PokemonStorage = ngp_clean_pc_data(ngp_storage, ngp_trainer.party) + end + onStartingNewGame() + end + + # Loads the game from the given save data and starts the map scene. + # @param save_data [Hash] hash containing the save data + # @raise [SaveData::InvalidValueError] if an invalid value is being loaded + def self.load(save_data) + validate save_data => Hash + SaveData.load_all_values(save_data) + self.load_map + pbAutoplayOnSave + $game_map.update + $PokemonMap.updateMap + $scene = Scene_Map.new + onLoadExistingGame() + end + + # Loads and validates the map. Called when loading a saved game. + def self.load_map + $game_map = $MapFactory.map + magic_number_matches = ($game_system.magic_number == $data_system.magic_number) + if !magic_number_matches || $PokemonGlobal.safesave + if pbMapInterpreterRunning? + pbMapInterpreter.setup(nil, 0) + end + begin + $MapFactory.setup($game_map.map_id) + rescue Errno::ENOENT + if $DEBUG + pbMessage(_INTL('Map {1} was not found.', $game_map.map_id)) + map = pbWarpToMapList + exit unless map + $MapFactory.setup(map[0]) + $game_player.moveto(map[1], map[2]) + else + raise _INTL('The map was not found. The game cannot continue.') + end + end + $game_player.center($game_player.x, $game_player.y) + else + $MapFactory.setMapChanged($game_map.map_id) + end + if $game_map.events.nil? + raise _INTL('The map is corrupt. The game cannot continue.') + end + $PokemonEncounters = PokemonEncounters.new + $PokemonEncounters.setup($game_map.map_id) + pbUpdateVehicle + end + + # Saves the game. Returns whether the operation was successful. + # @param save_file [String] the save file path + # @param safe [Boolean] whether $PokemonGlobal.safesave should be set to true + # @return [Boolean] whether the operation was successful + # @raise [SaveData::InvalidValueError] if an invalid value is being saved + def self.save(save_file = SaveData::FILE_PATH, safe: false) + validate save_file => String, safe => [TrueClass, FalseClass] + $PokemonGlobal.safesave = safe + $game_system.save_count += 1 + $game_system.magic_number = $data_system.magic_number + begin + SaveData.save_to_file(save_file) + Graphics.frame_reset + rescue IOError, SystemCallError + $game_system.save_count -= 1 + return false + end + return true + end +end diff --git a/Data/Scripts/003_Game processing/002_Scene_Map.rb b/Data/Scripts/003_Game processing/002_Scene_Map.rb new file mode 100644 index 000000000..63bec0c5f --- /dev/null +++ b/Data/Scripts/003_Game processing/002_Scene_Map.rb @@ -0,0 +1,292 @@ +#=============================================================================== +# ** Modified Scene_Map class for Pokémon. +#------------------------------------------------------------------------------- +# +#=============================================================================== +class Scene_Map + attr_reader :spritesetGlobal + attr_reader :map_renderer + attr_accessor :spritesets + + def spriteset + for i in @spritesets.values + return i if i.map == $game_map + end + return @spritesets.values[0] + end + + def createSpritesets + @map_renderer = TilemapRenderer.new(Spriteset_Map.viewport) if !@map_renderer || @map_renderer.disposed? + @spritesetGlobal = Spriteset_Global.new if !@spritesetGlobal + @spritesets = {} + for map in $MapFactory.maps + @spritesets[map.map_id] = Spriteset_Map.new(map) + end + $MapFactory.setSceneStarted(self) + updateSpritesets(true) + end + + def createSingleSpriteset(map) + temp = $scene.spriteset.getAnimations + @spritesets[map] = Spriteset_Map.new($MapFactory.maps[map]) + $scene.spriteset.restoreAnimations(temp) + $MapFactory.setSceneStarted(self) + updateSpritesets(true) + end + + def disposeSpritesets + return if !@spritesets + for i in @spritesets.keys + next if !@spritesets[i] + @spritesets[i].dispose + @spritesets[i] = nil + end + @spritesets.clear + @spritesets = {} + end + + def dispose + disposeSpritesets + @map_renderer.dispose + @map_renderer = nil + @spritesetGlobal.dispose + @spritesetGlobal = nil + end + + def autofade(mapid) + playingBGM = $game_system.playing_bgm + playingBGS = $game_system.playing_bgs + return if playingBGM && playingBGM.name == "ultra_metropolis" && darknessEffectOnMap(mapid) + return if !playingBGM && !playingBGS + map = load_data(sprintf("Data/Map%03d.rxdata", mapid)) + if playingBGM && map.autoplay_bgm + if (PBDayNight.isNight? rescue false) + pbBGMFade(0.8) if playingBGM.name != map.bgm.name && playingBGM.name != map.bgm.name + "_n" + else + pbBGMFade(0.8) if playingBGM.name != map.bgm.name + end + end + if playingBGS && map.autoplay_bgs + pbBGMFade(0.8) if playingBGS.name != map.bgs.name + end + Graphics.frame_reset + end + + #todo + def cacheNeedsClearing + return RPG::Cache.size >= 100 + end + + def reset_switches_for_map_transfer + $game_switches[SWITCH_ILEX_FOREST_SPOOKED_POKEMON] = false + end + + def clear_quest_icons() + for sprite in $scene.spriteset.character_sprites + if sprite.is_a?(Sprite_Character) && sprite.questIcon + sprite.removeQuestIcon + end + end + end + + def transfer_player(cancelVehicles = true) + reset_switches_for_map_transfer() + $game_temp.player_transferring = false + pbCancelVehicles($game_temp.player_new_map_id) if cancelVehicles + autofade($game_temp.player_new_map_id) + pbBridgeOff + @spritesetGlobal.playersprite.clearShadows + clear_quest_icons() + if $game_map.map_id != $game_temp.player_new_map_id + $MapFactory.setup($game_temp.player_new_map_id) + end + $game_player.moveto($game_temp.player_new_x, $game_temp.player_new_y) + case $game_temp.player_new_direction + when 2 then + $game_player.turn_down + when 4 then + $game_player.turn_left + when 6 then + $game_player.turn_right + when 8 then + $game_player.turn_up + end + + $game_player.straighten + $game_map.update + disposeSpritesets + if RPG::Cache.need_clearing + RPG::Cache.clear + end + createSpritesets + if $game_temp.transition_processing + $game_temp.transition_processing = false + Graphics.transition(20) + end + $game_map.autoplay + Graphics.frame_reset + Input.update + end + + def call_menu + $game_temp.menu_calling = false + $game_temp.in_menu = true + $game_player.straighten + $game_map.update + sscene = PokemonPauseMenu_Scene.new + sscreen = PokemonPauseMenu.new(sscene) + sscreen.pbStartPokemonMenu + $game_temp.in_menu = false + end + + def call_debug + $game_temp.debug_calling = false + pbPlayDecisionSE + $game_player.straighten + pbFadeOutIn { pbDebugMenu } + end + + def miniupdate + $PokemonTemp.miniupdate = true + loop do + $game_player.update + updateMaps + $game_system.update + $game_screen.update + break unless $game_temp.player_transferring + transfer_player + break if $game_temp.transition_processing + end + updateSpritesets + $PokemonTemp.miniupdate = false + end + + def updateMaps + for map in $MapFactory.maps + map.update + end + $MapFactory.updateMaps(self) + end + + def updateSpritesets(refresh = false) + @spritesets = {} if !@spritesets + keys = @spritesets.keys.clone + for i in keys + if !$MapFactory.hasMap?(i) + @spritesets[i].dispose if @spritesets[i] + @spritesets[i] = nil + @spritesets.delete(i) + else + @spritesets[i].update + end + end + @spritesetGlobal.update + for map in $MapFactory.maps + @spritesets[map.map_id] = Spriteset_Map.new(map) if !@spritesets[map.map_id] + end + pbDayNightTint(@map_renderer) + @map_renderer.refresh if refresh + @map_renderer.update + Events.onMapUpdate.trigger(self) + end + + def update + loop do + pbMapInterpreter.update + $game_player.update + updateMaps + $game_system.update + $game_screen.update + break unless $game_temp.player_transferring + transfer_player + break if $game_temp.transition_processing + end + updateSpritesets + if $game_temp.to_title + $game_temp.to_title = false + SaveData.mark_values_as_unloaded + $scene = pbCallTitle + return + end + if $game_temp.transition_processing + $game_temp.transition_processing = false + if $game_temp.transition_name == "" + Graphics.transition(20) + else + Graphics.transition(40, "Graphics/Transitions/" + $game_temp.transition_name) + end + end + return if $game_temp.message_window_showing + if !pbMapInterpreterRunning? + if Input.trigger?(Input::USE) + $PokemonTemp.hiddenMoveEventCalling = true + elsif Input.trigger?(Input::BACK) + unless $game_system.menu_disabled || $game_player.moving? + $game_temp.menu_calling = true + $game_temp.menu_beep = true + dayOfWeek = getDayOfTheWeek().to_s + $scene.spriteset.addUserSprite(LocationWindow.new($game_map.name+ "\n"+ pbGetTimeNow.strftime("%I:%M %p") + "\n" + dayOfWeek)) + end + elsif Input.trigger?(Input::SPECIAL) + unless $game_system.menu_disabled || $game_player.moving? + $PokemonTemp.keyItemCalling = true + end + elsif Input.press?(Input::F9) + $game_temp.debug_calling = true if $DEBUG + end + end + unless $game_player.moving? + if $game_temp.menu_calling + call_menu + elsif $game_temp.debug_calling + call_debug + elsif $PokemonTemp.keyItemCalling + $PokemonTemp.keyItemCalling = false + $game_player.straighten + pbUseKeyItem + elsif $PokemonTemp.hiddenMoveEventCalling + $PokemonTemp.hiddenMoveEventCalling = false + $game_player.straighten + Events.onAction.trigger(self) + end + end + end + + def reset_player_sprite + @spritesetGlobal.playersprite.updateBitmap + end + + def reset_map(fadeout = false,reset_music=true) + $MapFactory.setup($game_map.map_id) + $game_player.moveto($game_player.x, $game_player.y) + $game_player.straighten + $game_map.update + disposeSpritesets + GC.start + createSpritesets + if fadeout + $game_temp.transition_processing = false + Graphics.transition(20) + end + $game_map.autoplay if reset_music + Graphics.frame_reset + Input.update + end + + def main + createSpritesets + Graphics.transition(20) + loop do + Graphics.update + Input.update + update + break if $scene != self + end + Graphics.freeze + dispose + if $game_temp.to_title + Graphics.transition(20) + Graphics.freeze + end + end +end diff --git a/Data/Scripts/003_Game processing/003_Interpreter.rb b/Data/Scripts/003_Game processing/003_Interpreter.rb new file mode 100644 index 000000000..89d007cf8 --- /dev/null +++ b/Data/Scripts/003_Game processing/003_Interpreter.rb @@ -0,0 +1,457 @@ +#=============================================================================== +# ** Interpreter +#------------------------------------------------------------------------------- +# This interpreter runs event commands. This class is used within the +# Game_System class and the Game_Event class. +#=============================================================================== +class Interpreter + #----------------------------------------------------------------------------- + # * Object Initialization + # depth : nest depth + # main : main flag + #----------------------------------------------------------------------------- + def initialize(depth = 0, main = false) + @depth = depth + @main = main + if depth > 100 + print("Common event call has exceeded maximum limit.") + exit + end + clear + end + + def inspect + str = super.chop + str << format(' @event_id: %d>', @event_id) + return str + end + + def clear + @map_id = 0 # map ID when starting up + @event_id = 0 # event ID + @message_waiting = false # waiting for message to end + @move_route_waiting = false # waiting for move completion + @wait_count = 0 # wait count + @child_interpreter = nil # child interpreter + @branch = {} # branch data + @buttonInput = false + end + #----------------------------------------------------------------------------- + # * Event Setup + # list : list of event commands + # event_id : event ID + #----------------------------------------------------------------------------- + def setup(list, event_id, map_id = nil) + clear + @map_id = map_id || $game_map.map_id + @event_id = event_id + @list = list + @index = 0 + @branch.clear + end + + def setup_starting_event + $game_map.refresh if $game_map.need_refresh + # Set up common event if one wants to start + if $game_temp.common_event_id > 0 + setup($data_common_events[$game_temp.common_event_id].list, 0) + $game_temp.common_event_id = 0 + return + end + # Check all map events for one that wants to start, and set it up + for event in $game_map.events.values + next if !event.starting + if event.trigger < 3 # Isn't autorun or parallel processing + event.lock + event.clear_starting + end + setup(event.list, event.id, event.map.map_id) + return + end + # Check all common events for one that is autorun, and set it up + for common_event in $data_common_events.compact + next if common_event.trigger != 1 || !$game_switches[common_event.switch_id] + setup(common_event.list, 0) + return + end + end + + def running? + return @list != nil + end + #----------------------------------------------------------------------------- + # * Frame Update + #----------------------------------------------------------------------------- + def update + @loop_count = 0 + loop do + @loop_count += 1 + if @loop_count > 100 # Call Graphics.update for freeze prevention + Graphics.update + @loop_count = 0 + end + # If this interpreter's map isn't the current map or connected to it, + # forget this interpreter's event ID + if $game_map.map_id != @map_id && !$MapFactory.areConnected?($game_map.map_id, @map_id) + @event_id = 0 + end + # Update child interpreter if one exists + if @child_interpreter + @child_interpreter.update + @child_interpreter = nil if !@child_interpreter.running? + return if @child_interpreter + end + # Do nothing if a message is being shown + return if @message_waiting + # Do nothing if any event or the player is in the middle of a move route + if @move_route_waiting + return if $game_player.move_route_forcing + for event in $game_map.events.values + return if event.move_route_forcing + end + @move_route_waiting = false + end + # Do nothing while waiting + if @wait_count > 0 + @wait_count -= 1 + return + end + # Do nothing if the pause menu is going to open + return if $game_temp.menu_calling + # If there are no commands in the list, try to find something that wants to run + if @list.nil? + setup_starting_event if @main + return if @list.nil? # Couldn't find anything that wants to run + end + # Execute the next command + return if execute_command == false + # Move to the next @index + @index += 1 + end + end + #----------------------------------------------------------------------------- + # * Execute script + #----------------------------------------------------------------------------- + def execute_script(script) + begin + result = eval(script) + return result + rescue Exception + e = $! + raise if e.is_a?(SystemExit) || "#{e.class}" == "Reset" + event = get_self + s = "Backtrace:\r\n" + message = pbGetExceptionMessage(e) + if e.is_a?(SyntaxError) + script.each_line { |line| + line.gsub!(/\s+$/, "") + if line[/^\s*\(/] + message += "\r\n***Line '#{line}' shouldn't begin with '('. Try\r\n" + message += "putting the '(' at the end of the previous line instead,\r\n" + message += "or using 'extendtext.exe'." + end + if line[/\:\:\s*$/] + message += "\r\n***Line '#{line}' can't end with '::'. Try putting\r\n" + message += "the next word on the same line, e.g. 'PBSpecies:" + ":MEW'" + end + } + else + for bt in e.backtrace[0, 10] + s += bt + "\r\n" + end + s.gsub!(/Section(\d+)/) { $RGSS_SCRIPTS[$1.to_i][1] } + end + message = "Exception: #{e.class}\r\nMessage: " + message + "\r\n" + message += "\r\n***Full script:\r\n#{script}\r\n" + if event && $game_map + map_name = ($game_map.name rescue nil) || "???" + err = "Script error in event #{event.id} (coords #{event.x},#{event.y}), map #{$game_map.map_id} (#{map_name}):\r\n" + err += "#{message}\r\n#{s}" + if e.is_a?(Hangup) + $EVENTHANGUPMSG = err + raise + end + elsif $game_map + map_name = ($game_map.name rescue nil) || "???" + err = "Script error in map #{$game_map.map_id} (#{map_name}):\r\n" + err += "#{message}\r\n#{s}" + if e.is_a?(Hangup) + $EVENTHANGUPMSG = err + raise + end + else + err = "Script error in interpreter:\r\n#{message}\r\n#{s}" + if e.is_a?(Hangup) + $EVENTHANGUPMSG = err + raise + end + end + raise err + end + end + #----------------------------------------------------------------------------- + # * Get Character + # parameter : parameter + #----------------------------------------------------------------------------- + def get_character(parameter = 0) + case parameter + when -1 # player + return $game_player + when 0 # this event + events = $game_map.events + return (events) ? events[@event_id] : nil + else # specific event + events = $game_map.events + return (events) ? events[parameter] : nil + end + end + + def get_player + return get_character(-1) + end + + def get_self + return get_character(0) + end + + def get_event(parameter) + return get_character(parameter) + end + #----------------------------------------------------------------------------- + # * Freezes all events on the map (for use at the beginning of common events) + #----------------------------------------------------------------------------- + def pbGlobalLock + $game_map.events.values.each { |event| event.minilock } + end + #----------------------------------------------------------------------------- + # * Unfreezes all events on the map (for use at the end of common events) + #----------------------------------------------------------------------------- + def pbGlobalUnlock + $game_map.events.values.each { |event| event.unlock } + end + #----------------------------------------------------------------------------- + # * Gets the next index in the interpreter, ignoring certain commands between messages + #----------------------------------------------------------------------------- + def pbNextIndex(index) + return -1 if !@list || @list.length == 0 + i = index + 1 + loop do + return i if i >= @list.length - 1 + case @list[i].code + when 118, 108, 408 # Label, Comment + i += 1 + when 413 # Repeat Above + i = pbRepeatAbove(i) + when 113 # Break Loop + i = pbBreakLoop(i) + when 119 # Jump to Label + newI = pbJumpToLabel(i, @list[i].parameters[0]) + i = (newI > i) ? newI : i + 1 + else + return i + end + end + end + + def pbRepeatAbove(index) + index = @list[index].indent + loop do + index -= 1 + return index + 1 if @list[index].indent == indent + end + end + + def pbBreakLoop(index) + indent = @list[index].indent + temp_index = index + loop do + temp_index += 1 + return index + 1 if temp_index >= @list.size - 1 + return temp_index + 1 if @list[temp_index].code == 413 && + @list[temp_index].indent < indent + end + end + + def pbJumpToLabel(index, label_name) + temp_index = 0 + loop do + return index + 1 if temp_index >= @list.size - 1 + return temp_index + 1 if @list[temp_index].code == 118 && + @list[temp_index].parameters[0] == label_name + temp_index += 1 + end + end + #----------------------------------------------------------------------------- + # * Various methods to be used in a script event command. + #----------------------------------------------------------------------------- + # Helper function that shows a picture in a script. + def pbShowPicture(number, name, origin, x, y, zoomX = 100, zoomY = 100, opacity = 255, blendType = 0) + number = number + ($game_temp.in_battle ? 50 : 0) + $game_screen.pictures[number].show(name, origin, x, y, zoomX, zoomY, opacity, blendType) + end + + # Erases an event and adds it to the list of erased events so that + # it can stay erased when the game is saved then loaded again. + def pbEraseThisEvent + if $game_map.events[@event_id] + if $game_map.events[@event_id].name[/cuttree/i] + pbSmashThisEvent() + end + $game_map.events[@event_id].erase + $PokemonMap.addErasedEvent(@event_id) if $PokemonMap + end + @index += 1 + return true + end + + # Runs a common event. + def pbCommonEvent(id) + common_event = $data_common_events[id] + return if !common_event + if $game_temp.in_battle + $game_system.battle_interpreter.setup(common_event.list, 0) + else + interp = Interpreter.new + interp.setup(common_event.list, 0) + loop do + Graphics.update + Input.update + interp.update + pbUpdateSceneMap + break if !interp.running? + end + end + end + + # Sets another event's self switch (eg. pbSetSelfSwitch(20, "A", true) ). + def pbSetSelfSwitch(eventid, switch_name, value, mapid = -1) + mapid = @map_id if mapid < 0 + old_value = $game_self_switches[[mapid, eventid, switch_name]] + $game_self_switches[[mapid, eventid, switch_name]] = value + if value != old_value && $MapFactory.hasMap?(mapid) + $MapFactory.getMap(mapid, false).need_refresh = true + end + end + + def tsOff?(c) + return get_self.tsOff?(c) + end + alias isTempSwitchOff? tsOff? + + def tsOn?(c) + return get_self.tsOn?(c) + end + alias isTempSwitchOn? tsOn? + + def setTempSwitchOn(c) + get_self.setTempSwitchOn(c) + end + + def setTempSwitchOff(c) + get_self.setTempSwitchOff(c) + end + + def getVariable(*arg) + if arg.length == 0 + return nil if !$PokemonGlobal.eventvars + return $PokemonGlobal.eventvars[[@map_id, @event_id]] + else + return $game_variables[arg[0]] + end + end + + def setVariable(*arg) + if arg.length == 1 + $PokemonGlobal.eventvars = {} if !$PokemonGlobal.eventvars + $PokemonGlobal.eventvars[[@map_id, @event_id]] = arg[0] + else + $game_variables[arg[0]] = arg[1] + $game_map.need_refresh = true + end + end + + def pbGetPokemon(id) + return $Trainer.party[pbGet(id)] + end + + def pbSetEventTime(*arg) + $PokemonGlobal.eventvars = {} if !$PokemonGlobal.eventvars + time = pbGetTimeNow + time = time.to_i + pbSetSelfSwitch(@event_id, "A", true) + $PokemonGlobal.eventvars[[@map_id, @event_id]] = time + for otherevt in arg + pbSetSelfSwitch(otherevt, "A", true) + $PokemonGlobal.eventvars[[@map_id, otherevt]] = time + end + end + + # Used in boulder events. Allows an event to be pushed. + def pbPushThisEvent + event = get_self + old_x = event.x + old_y = event.y + # Apply strict version of passable, which treats tiles that are passable + # only from certain directions as fully impassible + # ^why?? - no + return if !event.can_move_in_direction?($game_player.direction, false) + case $game_player.direction + when 2 then event.move_down + when 4 then event.move_left + when 6 then event.move_right + when 8 then event.move_up + end + + if old_x != event.x || old_y != event.y + $game_player.lock + loop do + Graphics.update + Input.update + pbUpdateSceneMap + break if !event.moving? + end + $game_player.unlock + end + end + + def pbPushThisBoulder + pbPushThisEvent if $PokemonMap.strengthUsed + return true + end + + def pbSmashThisEvent + event = get_self + pbSmashEvent(event) if event + @index += 1 + return true + end + + def pbTrainerIntro(symbol) + return true if $DEBUG && !GameData::TrainerType.exists?(symbol) + tr_type = GameData::TrainerType.get(symbol).id + pbGlobalLock + pbPlayTrainerIntroME(tr_type) + return true + end + + def pbTrainerEnd + pbGlobalUnlock + event = get_self + event.erase_route if event + end + + def setPrice(item, buy_price = -1, sell_price = -1) + item = GameData::Item.get(item).id + $game_temp.mart_prices[item] = [-1, -1] if !$game_temp.mart_prices[item] + $game_temp.mart_prices[item][0] = buy_price if buy_price > 0 + if sell_price >= 0 # 0=can't sell + $game_temp.mart_prices[item][1] = sell_price * 2 + else + $game_temp.mart_prices[item][1] = buy_price if buy_price > 0 + end + end + + def setSellPrice(item, sell_price) + setPrice(item, -1, sell_price) + end +end diff --git a/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb b/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb new file mode 100644 index 000000000..710fbc8c6 --- /dev/null +++ b/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb @@ -0,0 +1,1027 @@ +#=============================================================================== +# ** Interpreter +#------------------------------------------------------------------------------- +# This interpreter runs event commands. This class is used within the +# Game_System class and the Game_Event class. +#=============================================================================== +class Interpreter + #----------------------------------------------------------------------------- + # * Event Command Execution + #----------------------------------------------------------------------------- + def execute_command + # Reached end of list of commands + if @index >= @list.size - 1 + command_end + return true + end + # Make current command's parameters available for reference via @parameters + @parameters = @list[@index].parameters + # Branch by command code + case @list[@index].code + when 101 then return command_101 # Show Text + when 102 then return command_102 # Show Choices + when 402 then return command_402 # When [**] + when 403 then return command_403 # When Cancel + when 103 then return command_103 # Input Number + when 104 then return command_104 # Change Text Options + when 105 then return command_105 # Button Input Processing + when 106 then return command_106 # Wait + when 111 then return command_111 # Conditional Branch + when 411 then return command_411 # Else + when 112 then return command_112 # Loop + when 413 then return command_413 # Repeat Above + when 113 then return command_113 # Break Loop + when 115 then return command_115 # Exit Event Processing + when 116 then return command_116 # Erase Event + when 117 then return command_117 # Call Common Event + when 118 then return command_118 # Label + when 119 then return command_119 # Jump to Label + when 121 then return command_121 # Control Switches + when 122 then return command_122 # Control Variables + when 123 then return command_123 # Control Self Switch + when 124 then return command_124 # Control Timer + when 125 then return command_125 # Change Gold + when 126 then return command_126 # Change Items + when 127 then return command_127 # Change Weapons + when 128 then return command_128 # Change Armor + when 129 then return command_129 # Change Party Member + when 131 then return command_131 # Change Windowskin + when 132 then return command_132 # Change Battle BGM + when 133 then return command_133 # Change Battle End ME + when 134 then return command_134 # Change Save Access + when 135 then return command_135 # Change Menu Access + when 136 then return command_136 # Change Encounter + when 201 then return command_201 # Transfer Player + when 202 then return command_202 # Set Event Location + when 203 then return command_203 # Scroll Map + when 204 then return command_204 # Change Map Settings + when 205 then return command_205 # Change Fog Color Tone + when 206 then return command_206 # Change Fog Opacity + when 207 then return command_207 # Show Animation + when 208 then return command_208 # Change Transparent Flag + when 209 then return command_209 # Set Move Route + when 210 then return command_210 # Wait for Move's Completion + when 221 then return command_221 # Prepare for Transition + when 222 then return command_222 # Execute Transition + when 223 then return command_223 # Change Screen Color Tone + when 224 then return command_224 # Screen Flash + when 225 then return command_225 # Screen Shake + when 231 then return command_231 # Show Picture + when 232 then return command_232 # Move Picture + when 233 then return command_233 # Rotate Picture + when 234 then return command_234 # Change Picture Color Tone + when 235 then return command_235 # Erase Picture + when 236 then return command_236 # Set Weather Effects + when 241 then return command_241 # Play BGM + when 242 then return command_242 # Fade Out BGM + when 245 then return command_245 # Play BGS + when 246 then return command_246 # Fade Out BGS + when 247 then return command_247 # Memorize BGM/BGS + when 248 then return command_248 # Restore BGM/BGS + when 249 then return command_249 # Play ME + when 250 then return command_250 # Play SE + when 251 then return command_251 # Stop SE + when 301 then return command_301 # Battle Processing + when 601 then return command_601 # If Win + when 602 then return command_602 # If Escape + when 603 then return command_603 # If Lose + when 302 then return command_302 # Shop Processing + when 303 then return command_303 # Name Input Processing + when 311 then return command_311 # Change HP + when 312 then return command_312 # Change SP + when 313 then return command_313 # Change State + when 314 then return command_314 # Recover All + when 315 then return command_315 # Change EXP + when 316 then return command_316 # Change Level + when 317 then return command_317 # Change Parameters + when 318 then return command_318 # Change Skills + when 319 then return command_319 # Change Equipment + when 320 then return command_320 # Change Actor Name + when 321 then return command_321 # Change Actor Class + when 322 then return command_322 # Change Actor Graphic + when 331 then return command_331 # Change Enemy HP + when 332 then return command_332 # Change Enemy SP + when 333 then return command_333 # Change Enemy State + when 334 then return command_334 # Enemy Recover All + when 335 then return command_335 # Enemy Appearance + when 336 then return command_336 # Enemy Transform + when 337 then return command_337 # Show Battle Animation + when 338 then return command_338 # Deal Damage + when 339 then return command_339 # Force Action + when 340 then return command_340 # Abort Battle + when 351 then return command_351 # Call Menu Screen + when 352 then return command_352 # Call Save Screen + when 353 then return command_353 # Game Over + when 354 then return command_354 # Return to Title Screen + when 355 then return command_355 # Script + else return true # Other + end + end + + def command_dummy + return true + end + #----------------------------------------------------------------------------- + # * End Event + #----------------------------------------------------------------------------- + def command_end + @list = nil + # If main map event and event ID are valid, unlock event + if @main && @event_id > 0 && $game_map.events[@event_id] + $game_map.events[@event_id].unlock + end + end + #----------------------------------------------------------------------------- + # * Command Skip + #----------------------------------------------------------------------------- + def command_skip + indent = @list[@index].indent + loop do + return true if @list[@index + 1].indent == indent + @index += 1 + end + end + #----------------------------------------------------------------------------- + # * Command If + #----------------------------------------------------------------------------- + def command_if(value) + if @branch[@list[@index].indent] == value + @branch.delete(@list[@index].indent) + return true + end + return command_skip + end + #----------------------------------------------------------------------------- + # * Show Text + #----------------------------------------------------------------------------- + def command_101 + return false if $game_temp.message_window_showing + message = @list[@index].parameters[0] + message_end = "" + commands = nil + number_input_variable = nil + number_input_max_digits = nil + # Check the next command(s) for things to add on to this text + loop do + next_index = pbNextIndex(@index) + case @list[next_index].code + when 401 # Continuation of 101 Show Text + text = @list[next_index].parameters[0] + message += " " if text != "" && message[message.length - 1, 1] != " " + message += text + @index = next_index + next + when 101 # Show Text + message_end = "\1" + when 102 # Show Choices + commands = @list[next_index].parameters + @index = next_index + when 103 # Input Number + number_input_variable = @list[next_index].parameters[0] + number_input_max_digits = @list[next_index].parameters[1] + @index = next_index + end + break + end + # Translate the text + message = _MAPINTL($game_map.map_id, message) + # Display the text, with commands/number choosing if appropriate + @message_waiting = true # Lets parallel process events work while a message is displayed + if commands + cmd_texts = [] + for cmd in commands[0] + cmd_texts.push(_MAPINTL($game_map.map_id, cmd)) + end + command = pbMessage(message + message_end, cmd_texts, commands[1]) + @branch[@list[@index].indent] = command + elsif number_input_variable + params = ChooseNumberParams.new + params.setMaxDigits(number_input_max_digits) + params.setDefaultValue($game_variables[number_input_variable]) + $game_variables[number_input_variable] = pbMessageChooseNumber(message + message_end, params) + $game_map.need_refresh = true if $game_map + else + pbMessage(message + message_end) + end + @message_waiting = false + return true + end + #----------------------------------------------------------------------------- + # * Show Choices + #----------------------------------------------------------------------------- + def command_102 + @message_waiting = true + command = pbShowCommands(nil, @list[@index].parameters[0], @list[@index].parameters[1]) + @message_waiting = false + @branch[@list[@index].indent] = command + Input.update # Must call Input.update again to avoid extra triggers + return true + end + #----------------------------------------------------------------------------- + # * When [**] + #----------------------------------------------------------------------------- + def command_402 + if @branch[@list[@index].indent] == @parameters[0] + @branch.delete(@list[@index].indent) + return true + end + return command_skip + end + #----------------------------------------------------------------------------- + # * When Cancel + #----------------------------------------------------------------------------- + def command_403 + if @branch[@list[@index].indent] == 4 + @branch.delete(@list[@index].indent) + return true + end + return command_skip + end + #----------------------------------------------------------------------------- + # * Input Number + #----------------------------------------------------------------------------- + def command_103 + @message_waiting = true + variable_number = @list[@index].parameters[0] + params = ChooseNumberParams.new + params.setMaxDigits(@list[@index].parameters[1]) + params.setDefaultValue($game_variables[variable_number]) + $game_variables[variable_number] = pbChooseNumber(nil, params) + $game_map.need_refresh = true if $game_map + @message_waiting = false + return true + end + #----------------------------------------------------------------------------- + # * Change Text Options + #----------------------------------------------------------------------------- + def command_104 + return false if $game_temp.message_window_showing + $game_system.message_position = @parameters[0] + $game_system.message_frame = @parameters[1] + return true + end + #----------------------------------------------------------------------------- + # * Button Input Processing + #----------------------------------------------------------------------------- + def pbButtonInputProcessing(variable_number = 0, timeout_frames = 0) + ret = 0 + timer = timeout_frames * Graphics.frame_rate / 20 + loop do + Graphics.update + Input.update + pbUpdateSceneMap + # Check for input and break if there is one + for i in 1..18 + ret = i if Input.trigger?(i) + end + break if ret != 0 + # Count down the timer and break if it runs out + if timeout_frames > 0 + timer -= 1 + break if timer <= 0 + end + end + Input.update + if variable_number && variable_number > 0 + $game_variables[variable_number] = ret + $game_map.need_refresh = true if $game_map + end + return ret + end + + def command_105 + return false if @buttonInput + @buttonInput = true + pbButtonInputProcessing(@list[@index].parameters[0]) + @buttonInput = false + @index += 1 + return true + end + #----------------------------------------------------------------------------- + # * Wait + #----------------------------------------------------------------------------- + def command_106 + @wait_count = @parameters[0] * Graphics.frame_rate / 20 + return true + end + #----------------------------------------------------------------------------- + # * Conditional Branch + #----------------------------------------------------------------------------- + def command_111 + result = false + case @parameters[0] + when 0 # switch + switch_name = $data_system.switches[@parameters[1]] + if switch_name && switch_name[/^s\:/] + result = (eval($~.post_match) == (@parameters[2] == 0)) + else + result = ($game_switches[@parameters[1]] == (@parameters[2] == 0)) + end + when 1 # variable + value1 = $game_variables[@parameters[1]] + value2 = (@parameters[2] == 0) ? @parameters[3] : $game_variables[@parameters[3]] + case @parameters[4] + when 0 then result = (value1 == value2) + when 1 then result = (value1 >= value2) + when 2 then result = (value1 <= value2) + when 3 then result = (value1 > value2) + when 4 then result = (value1 < value2) + when 5 then result = (value1 != value2) + end + when 2 # self switch + if @event_id > 0 + key = [$game_map.map_id, @event_id, @parameters[1]] + result = ($game_self_switches[key] == (@parameters[2] == 0)) + end + when 3 # timer + if $game_system.timer_working + sec = $game_system.timer / Graphics.frame_rate + result = (@parameters[2] == 0) ? (sec >= @parameters[1]) : (sec <= @parameters[1]) + end +# when 4, 5 # actor, enemy + when 6 # character + character = get_character(@parameters[1]) + result = (character.direction == @parameters[2]) if character + when 7 # gold + gold = $Trainer.money + result = (@parameters[2] == 0) ? (gold >= @parameters[1]) : (gold <= @parameters[1]) +# when 8, 9, 10 # item, weapon, armor + when 11 # button + result = Input.press?(@parameters[1]) + when 12 # script + result = execute_script(@parameters[1]) + end + # Store result in hash + @branch[@list[@index].indent] = result + if @branch[@list[@index].indent] + @branch.delete(@list[@index].indent) + return true + end + return command_skip + end + #----------------------------------------------------------------------------- + # * Else + #----------------------------------------------------------------------------- + def command_411 + if @branch[@list[@index].indent] == false # Could be nil, so intentionally checks for false + @branch.delete(@list[@index].indent) + return true + end + return command_skip + end + #----------------------------------------------------------------------------- + # * Loop + #----------------------------------------------------------------------------- + def command_112 + return true + end + #----------------------------------------------------------------------------- + # * Repeat Above + #----------------------------------------------------------------------------- + def command_413 + indent = @list[@index].indent + loop do + @index -= 1 + return true if @list[@index].indent == indent + end + end + #----------------------------------------------------------------------------- + # * Break Loop + #----------------------------------------------------------------------------- + def command_113 + indent = @list[@index].indent + temp_index = @index + loop do + temp_index += 1 + return true if temp_index >= @list.size - 1 # Reached end of commands + # Skip ahead to after the [Repeat Above] end of the current loop + if @list[temp_index].code == 413 && @list[temp_index].indent < indent + @index = temp_index + return true + end + end + end + #----------------------------------------------------------------------------- + # * Exit Event Processing + #----------------------------------------------------------------------------- + def command_115 + command_end + return true + end + #----------------------------------------------------------------------------- + # * Erase Event + #----------------------------------------------------------------------------- + def command_116 + if @event_id > 0 + $game_map.events[@event_id].erase if $game_map.events[@event_id] + $PokemonMap.addErasedEvent(@event_id) if $PokemonMap + end + @index += 1 + return false + end + #----------------------------------------------------------------------------- + # * Call Common Event + #----------------------------------------------------------------------------- + def command_117 + common_event = $data_common_events[@parameters[0]] + if common_event + @child_interpreter = Interpreter.new(@depth + 1) + @child_interpreter.setup(common_event.list, @event_id) + end + return true + end + #----------------------------------------------------------------------------- + # * Label + #----------------------------------------------------------------------------- + def command_118 + return true + end + #----------------------------------------------------------------------------- + # * Jump to Label + #----------------------------------------------------------------------------- + def command_119 + label_name = @parameters[0] + temp_index = 0 + loop do + return true if temp_index >= @list.size - 1 # Reached end of commands + # Check whether this command is a label with the desired name + if @list[temp_index].code == 118 && + @list[temp_index].parameters[0] == label_name + @index = temp_index + return true + end + # Command isn't the desired label, increment temp_index and keep looking + temp_index += 1 + end + end + #----------------------------------------------------------------------------- + # * Control Switches + #----------------------------------------------------------------------------- + def command_121 + should_refresh = false + for i in @parameters[0]..@parameters[1] + next if $game_switches[i] == (@parameters[2] == 0) + $game_switches[i] = (@parameters[2] == 0) + should_refresh = true + end + # Refresh map + $game_map.need_refresh = true if should_refresh + return true + end + #----------------------------------------------------------------------------- + # * Control Variables + #----------------------------------------------------------------------------- + def command_122 + value = 0 + case @parameters[3] + when 0 # invariable (fixed value) + value = @parameters[4] + when 1 # variable + value = $game_variables[@parameters[4]] + when 2 # random number + value = @parameters[4] + rand(@parameters[5] - @parameters[4] + 1) +# when 3, 4, 5 # item, actor, enemy + when 6 # character + character = get_character(@parameters[4]) + if character + case @parameters[5] + when 0 then value = character.x # x-coordinate + when 1 then value = character.y # y-coordinate + when 2 then value = character.direction # direction + when 3 then value = character.screen_x # screen x-coordinate + when 4 then value = character.screen_y # screen y-coordinate + when 5 then value = character.terrain_tag.id_number # terrain tag + end + end + when 7 # other + case @parameters[4] + when 0 then value = $game_map.map_id # map ID + when 1 then value = $Trainer.pokemon_party.length # party members + when 2 then value = $Trainer.money # gold +# when 3 # steps + when 4 then value = Graphics.frame_count / Graphics.frame_rate # play time + when 5 then value = $game_system.timer / Graphics.frame_rate # timer + when 6 then value = $game_system.save_count # save count + end + end + # Apply value and operation to all specified game variables + for i in @parameters[0]..@parameters[1] + case @parameters[2] + when 0 # set + next if $game_variables[i] == value + $game_variables[i] = value + when 1 # add + next if $game_variables[i] >= 99999999 + $game_variables[i] += value + when 2 # subtract + next if $game_variables[i] <= -99999999 + $game_variables[i] -= value + when 3 # multiply + next if value == 1 + $game_variables[i] *= value + when 4 # divide + next if value == 1 || value == 0 + $game_variables[i] /= value + when 5 # remainder + next if value == 1 || value == 0 + $game_variables[i] %= value + end + $game_variables[i] = 99999999 if $game_variables[i] > 99999999 + $game_variables[i] = -99999999 if $game_variables[i] < -99999999 + $game_map.need_refresh = true + end + return true + end + #----------------------------------------------------------------------------- + # * Control Self Switch + #----------------------------------------------------------------------------- + def command_123 + if @event_id > 0 + new_value = (@parameters[1] == 0) + key = [$game_map.map_id, @event_id, @parameters[0]] + if $game_self_switches[key] != new_value + $game_self_switches[key] = new_value + $game_map.need_refresh = true + end + end + return true + end + #----------------------------------------------------------------------------- + # * Control Timer + #----------------------------------------------------------------------------- + def command_124 + $game_system.timer_working = (@parameters[0] == 0) + $game_system.timer = @parameters[1] * Graphics.frame_rate if @parameters[0] == 0 + return true + end + #----------------------------------------------------------------------------- + # * Change Gold + #----------------------------------------------------------------------------- + def command_125 + value = (@parameters[1] == 0) ? @parameters[2] : $game_variables[@parameters[2]] + value = -value if @parameters[0] == 1 # Decrease + $Trainer.money += value + return true + end + + def command_126; command_dummy; end # Change Items + def command_127; command_dummy; end # Change Weapons + def command_128; command_dummy; end # Change Armor + def command_129; command_dummy; end # Change Party Member + #----------------------------------------------------------------------------- + # * Change Windowskin + #----------------------------------------------------------------------------- + def command_131 + for i in 0...Settings::SPEECH_WINDOWSKINS.length + next if Settings::SPEECH_WINDOWSKINS[i] != @parameters[0] + $PokemonSystem.textskin = i + MessageConfig.pbSetSpeechFrame("Graphics/Windowskins/" + Settings::SPEECH_WINDOWSKINS[i]) + return true + end + return true + end + #----------------------------------------------------------------------------- + # * Change Battle BGM + #----------------------------------------------------------------------------- + def command_132 + ($PokemonGlobal.nextBattleBGM = @parameters[0]) ? @parameters[0].clone : nil + return true + end + #----------------------------------------------------------------------------- + # * Change Battle End ME + #----------------------------------------------------------------------------- + def command_133 + ($PokemonGlobal.nextBattleME = @parameters[0]) ? @parameters[0].clone : nil + return true + end + #----------------------------------------------------------------------------- + # * Change Save Access + #----------------------------------------------------------------------------- + def command_134 + $game_system.save_disabled = (@parameters[0] == 0) + return true + end + #----------------------------------------------------------------------------- + # * Change Menu Access + #----------------------------------------------------------------------------- + def command_135 + $game_system.menu_disabled = (@parameters[0] == 0) + return true + end + #----------------------------------------------------------------------------- + # * Change Encounter + #----------------------------------------------------------------------------- + def command_136 + $game_system.encounter_disabled = (@parameters[0] == 0) + $game_player.make_encounter_count + return true + end + #----------------------------------------------------------------------------- + # * Transfer Player + #----------------------------------------------------------------------------- + def command_201 + return true if $game_temp.in_battle + return false if $game_temp.player_transferring || + $game_temp.message_window_showing || + $game_temp.transition_processing + # Set up the transfer and the player's new coordinates + $game_temp.player_transferring = true + if @parameters[0] == 0 # Direct appointment + $game_temp.player_new_map_id = @parameters[1] + $game_temp.player_new_x = @parameters[2] + $game_temp.player_new_y = @parameters[3] + $game_temp.player_new_direction = @parameters[4] + else # Appoint with variables + $game_temp.player_new_map_id = $game_variables[@parameters[1]] + $game_temp.player_new_x = $game_variables[@parameters[2]] + $game_temp.player_new_y = $game_variables[@parameters[3]] + $game_temp.player_new_direction = @parameters[4] + end + @index += 1 + # If transition happens with a fade, do the fade + if @parameters[5] == 0 + Graphics.freeze + $game_temp.transition_processing = true + $game_temp.transition_name = "" + end + return false + end + #----------------------------------------------------------------------------- + # * Set Event Location + #----------------------------------------------------------------------------- + def command_202 + return true if $game_temp.in_battle + character = get_character(@parameters[0]) + return true if character.nil? + # Move the character + if @parameters[1] == 0 # Direct appointment + character.moveto(@parameters[2], @parameters[3]) + elsif @parameters[1] == 1 # Appoint with variables + character.moveto($game_variables[@parameters[2]], $game_variables[@parameters[3]]) + else # Exchange with another event + character2 = get_character(@parameters[2]) + if character2 + old_x = character.x + old_y = character.y + character.moveto(character2.x, character2.y) + character2.moveto(old_x, old_y) + end + end + # Set character's direction + case @parameters[4] + when 2 then character.turn_down + when 4 then character.turn_left + when 6 then character.turn_right + when 8 then character.turn_up + end + return true + end + #----------------------------------------------------------------------------- + # * Scroll Map + #----------------------------------------------------------------------------- + def command_203 + return true if $game_temp.in_battle + return false if $game_map.scrolling? + $game_map.start_scroll(@parameters[0], @parameters[1], @parameters[2]) + return true + end + #----------------------------------------------------------------------------- + # * Change Map Settings + #----------------------------------------------------------------------------- + def command_204 + case @parameters[0] + when 0 # panorama + $game_map.panorama_name = @parameters[1] + $game_map.panorama_hue = @parameters[2] + when 1 # fog + $game_map.fog_name = @parameters[1] + $game_map.fog_hue = @parameters[2] + $game_map.fog_opacity = @parameters[3] + $game_map.fog_blend_type = @parameters[4] + $game_map.fog_zoom = @parameters[5] + $game_map.fog_sx = @parameters[6] + $game_map.fog_sy = @parameters[7] + when 2 # battleback + $game_map.battleback_name = @parameters[1] + $game_temp.battleback_name = @parameters[1] + end + return true + end + #----------------------------------------------------------------------------- + # * Change Fog Color Tone + #----------------------------------------------------------------------------- + def command_205 + $game_map.start_fog_tone_change(@parameters[0], @parameters[1] * Graphics.frame_rate / 20) + return true + end + #----------------------------------------------------------------------------- + # * Change Fog Opacity + #----------------------------------------------------------------------------- + def command_206 + $game_map.start_fog_opacity_change(@parameters[0], @parameters[1] * Graphics.frame_rate / 20) + return true + end + #----------------------------------------------------------------------------- + # * Show Animation + #----------------------------------------------------------------------------- + def command_207 + character = get_character(@parameters[0]) + return true if character.nil? + character.animation_id = @parameters[1] + return true + end + #----------------------------------------------------------------------------- + # * Change Transparent Flag + #----------------------------------------------------------------------------- + def command_208 + $game_player.transparent = (@parameters[0] == 0) + return true + end + #----------------------------------------------------------------------------- + # * Set Move Route + #----------------------------------------------------------------------------- + def command_209 + character = get_character(@parameters[0]) + return true if character.nil? + character.force_move_route(@parameters[1]) + return true + end + #----------------------------------------------------------------------------- + # * Wait for Move's Completion + #----------------------------------------------------------------------------- + def command_210 + @move_route_waiting = true if !$game_temp.in_battle + return true + end + #----------------------------------------------------------------------------- + # * Prepare for Transition + #----------------------------------------------------------------------------- + def command_221 + return false if $game_temp.message_window_showing + Graphics.freeze + return true + end + #----------------------------------------------------------------------------- + # * Execute Transition + #----------------------------------------------------------------------------- + def command_222 + return false if $game_temp.transition_processing + $game_temp.transition_processing = true + $game_temp.transition_name = @parameters[0] + @index += 1 + return false + end + #----------------------------------------------------------------------------- + # * Change Screen Color Tone + #----------------------------------------------------------------------------- + def command_223 + $game_screen.start_tone_change(@parameters[0], @parameters[1] * Graphics.frame_rate / 20) + return true + end + #----------------------------------------------------------------------------- + # * Screen Flash + #----------------------------------------------------------------------------- + def command_224 + $game_screen.start_flash(@parameters[0], @parameters[1] * Graphics.frame_rate / 20) + return true + end + #----------------------------------------------------------------------------- + # * Screen Shake + #----------------------------------------------------------------------------- + def command_225 + $game_screen.start_shake(@parameters[0], @parameters[1], @parameters[2] * Graphics.frame_rate / 20) + return true + end + #----------------------------------------------------------------------------- + # * Show Picture + #----------------------------------------------------------------------------- + def command_231 + number = @parameters[0] + ($game_temp.in_battle ? 50 : 0) + if @parameters[3] == 0 # Direct appointment + x = @parameters[4] + y = @parameters[5] + else # Appoint with variables + x = $game_variables[@parameters[4]] + y = $game_variables[@parameters[5]] + end + $game_screen.pictures[number].show(@parameters[1], @parameters[2], + x, y, @parameters[6], @parameters[7], @parameters[8], @parameters[9]) + return true + end + #----------------------------------------------------------------------------- + # * Move Picture + #----------------------------------------------------------------------------- + def command_232 + number = @parameters[0] + ($game_temp.in_battle ? 50 : 0) + if @parameters[3] == 0 # Direct appointment + x = @parameters[4] + y = @parameters[5] + else # Appoint with variables + x = $game_variables[@parameters[4]] + y = $game_variables[@parameters[5]] + end + $game_screen.pictures[number].move(@parameters[1] * Graphics.frame_rate / 20, + @parameters[2], x, y, @parameters[6], @parameters[7], @parameters[8], @parameters[9]) + return true + end + #----------------------------------------------------------------------------- + # * Rotate Picture + #----------------------------------------------------------------------------- + def command_233 + number = @parameters[0] + ($game_temp.in_battle ? 50 : 0) + $game_screen.pictures[number].rotate(@parameters[1]) + return true + end + #----------------------------------------------------------------------------- + # * Change Picture Color Tone + #----------------------------------------------------------------------------- + def command_234 + number = @parameters[0] + ($game_temp.in_battle ? 50 : 0) + $game_screen.pictures[number].start_tone_change(@parameters[1], + @parameters[2] * Graphics.frame_rate / 20) + return true + end + #----------------------------------------------------------------------------- + # * Erase Picture + #----------------------------------------------------------------------------- + def command_235 + number = @parameters[0] + ($game_temp.in_battle ? 50 : 0) + $game_screen.pictures[number].erase + return true + end + #----------------------------------------------------------------------------- + # * Set Weather Effects + #----------------------------------------------------------------------------- + def command_236 + $game_screen.weather(@parameters[0], @parameters[1], @parameters[2]) + return true + end + #----------------------------------------------------------------------------- + # * Play BGM + #----------------------------------------------------------------------------- + def command_241 + pbBGMPlay(@parameters[0]) + return true + end + #----------------------------------------------------------------------------- + # * Fade Out BGM + #----------------------------------------------------------------------------- + def command_242 + pbBGMFade(@parameters[0]) + return true + end + #----------------------------------------------------------------------------- + # * Play BGS + #----------------------------------------------------------------------------- + def command_245 + pbBGSPlay(@parameters[0]) + return true + end + #----------------------------------------------------------------------------- + # * Fade Out BGS + #----------------------------------------------------------------------------- + def command_246 + pbBGSFade(@parameters[0]) + return true + end + #----------------------------------------------------------------------------- + # * Memorize BGM/BGS + #----------------------------------------------------------------------------- + def command_247 + $game_system.bgm_memorize + $game_system.bgs_memorize + return true + end + #----------------------------------------------------------------------------- + # * Restore BGM/BGS + #----------------------------------------------------------------------------- + def command_248 + $game_system.bgm_restore + $game_system.bgs_restore + return true + end + #----------------------------------------------------------------------------- + # * Play ME + #----------------------------------------------------------------------------- + def command_249 + pbMEPlay(@parameters[0]) + return true + end + #----------------------------------------------------------------------------- + # * Play SE + #----------------------------------------------------------------------------- + def command_250 + pbSEPlay(@parameters[0]) + return true + end + #----------------------------------------------------------------------------- + # * Stop SE + #----------------------------------------------------------------------------- + def command_251 + pbSEStop + return true + end + + def command_301; command_dummy; end # Battle Processing + def command_601; command_if(0); end # If Win + def command_602; command_if(1); end # If Escape + def command_603; command_if(2); end # If Lose + def command_302; command_dummy; end # Shop Processing + #----------------------------------------------------------------------------- + # * Name Input Processing + #----------------------------------------------------------------------------- + def command_303 + if $Trainer + $Trainer.name = pbEnterPlayerName(_INTL("Your name?"), 1, @parameters[1], $Trainer.name) + return true + end + if $game_actors && $data_actors && $data_actors[@parameters[0]] != nil + $game_temp.battle_abort = true + pbFadeOutIn { + sscene = PokemonEntryScene.new + sscreen = PokemonEntry.new(sscene) + $game_actors[@parameters[0]].name = sscreen.pbStartScreen( + _INTL("Enter {1}'s name.", $game_actors[@parameters[0]].name), + 1, @parameters[1], $game_actors[@parameters[0]].name) + } + end + return true + end + + def command_311; command_dummy; end # Change HP + def command_312; command_dummy; end # Change SP + def command_313; command_dummy; end # Change State + #----------------------------------------------------------------------------- + # * Recover All + #----------------------------------------------------------------------------- + def command_314 + $Trainer.heal_party if @parameters[0] == 0 + return true + end + + def command_315; command_dummy; end # Change EXP + def command_316; command_dummy; end # Change Level + def command_317; command_dummy; end # Change Parameters + def command_318; command_dummy; end # Change Skills + def command_319; command_dummy; end # Change Equipment + def command_320; command_dummy; end # Change Actor Name + def command_321; command_dummy; end # Change Actor Class + def command_322; command_dummy; end # Change Actor Graphic + def command_331; command_dummy; end # Change Enemy HP + def command_332; command_dummy; end # Change Enemy SP + def command_333; command_dummy; end # Change Enemy State + def command_334; command_dummy; end # Enemy Recover All + def command_335; command_dummy; end # Enemy Appearance + def command_336; command_dummy; end # Enemy Transform + def command_337; command_dummy; end # Show Battle Animation + def command_338; command_dummy; end # Deal Damage + def command_339; command_dummy; end # Force Action + def command_340; command_dummy; end # Abort Battle + #----------------------------------------------------------------------------- + # * Call Menu Screen + #----------------------------------------------------------------------------- + def command_351 + $game_temp.menu_calling = true + @index += 1 + return false + end + #----------------------------------------------------------------------------- + # * Call Save Screen + #----------------------------------------------------------------------------- + def command_352 + scene = PokemonSave_Scene.new + screen = PokemonSaveScreen.new(scene) + screen.pbSaveScreen + return true + end + #----------------------------------------------------------------------------- + # * Game Over + #----------------------------------------------------------------------------- + def command_353 + pbBGMFade(1.0) + pbBGSFade(1.0) + pbFadeOutIn { pbStartOver(true) } + end + #----------------------------------------------------------------------------- + # * Return to Title Screen + #----------------------------------------------------------------------------- + def command_354 + $game_temp.to_title = true + return false + end + #----------------------------------------------------------------------------- + # * Script + #----------------------------------------------------------------------------- + def command_355 + script = @list[@index].parameters[0] + "\n" + # Look for more script commands or a continuation of one, and add them to script + loop do + break if ![355, 655].include?(@list[@index + 1].code) + script += @list[@index+1].parameters[0] + "\n" + @index += 1 + end + # Run the script + execute_script(script) + return true + end +end diff --git a/Data/Scripts/003_Game processing/005_Event_Handlers.rb b/Data/Scripts/003_Game processing/005_Event_Handlers.rb new file mode 100644 index 000000000..58129853f --- /dev/null +++ b/Data/Scripts/003_Game processing/005_Event_Handlers.rb @@ -0,0 +1,275 @@ +#=============================================================================== +# Defines an event that procedures can subscribe to. +#=============================================================================== +class Event + def initialize + @callbacks = [] + end + + # Sets an event handler for this event and removes all other event handlers. + def set(method) + @callbacks.clear + @callbacks.push(method) + end + + # Removes an event handler procedure from the event. + def -(method) + for i in 0...@callbacks.length + next if @callbacks[i]!=method + @callbacks.delete_at(i) + break + end + return self + end + + # Adds an event handler procedure from the event. + def +(method) + for i in 0...@callbacks.length + return self if @callbacks[i]==method + end + @callbacks.push(method) + return self + end + + # Clears the event of event handlers. + def clear + @callbacks.clear + end + + # Triggers the event and calls all its event handlers. Normally called only + # by the code where the event occurred. + # The first argument is the sender of the event, the second argument contains + # the event's parameters. If three or more arguments are given, this method + # supports the following callbacks: + # proc { |sender,params| } where params is an array of the other parameters, and + # proc { |sender,arg0,arg1,...| } + def trigger(*arg) + arglist = arg[1,arg.length] + for callback in @callbacks + if callback.arity>2 && arg.length==callback.arity + # Retrofitted for callbacks that take three or more arguments + callback.call(*arg) + else + callback.call(arg[0],arglist) + end + end + end + + # Triggers the event and calls all its event handlers. Normally called only + # by the code where the event occurred. The first argument is the sender of + # the event, the other arguments are the event's parameters. + def trigger2(*arg) + for callback in @callbacks + callback.call(*arg) + end + end +end + +#=============================================================================== +# +#=============================================================================== +class HandlerHash + def initialize(mod) + @mod = mod + @hash = {} + @addIfs = [] + @symbolCache = {} + end + + def fromSymbol(sym) + return sym unless sym.is_a?(Symbol) || sym.is_a?(String) + mod = Object.const_get(@mod) rescue nil + return nil if !mod + return mod.const_get(sym.to_sym) rescue nil + end + + def toSymbol(sym) + return sym.to_sym if sym.is_a?(Symbol) || sym.is_a?(String) + ret = @symbolCache[sym] + return ret if ret + mod = Object.const_get(@mod) rescue nil + return nil if !mod + for key in mod.constants + next if mod.const_get(key)!=sym + ret = key.to_sym + @symbolCache[sym] = ret + break + end + return ret + end + + def addIf(conditionProc,handler=nil,&handlerBlock) + if ![Proc,Hash].include?(handler.class) && !block_given? + raise ArgumentError, "addIf call for #{self.class.name} has no valid handler (#{handler.inspect} was given)" + end + @addIfs.push([conditionProc,handler || handlerBlock]) + end + + def add(sym,handler=nil,&handlerBlock) # 'sym' can be an ID or symbol + if ![Proc,Hash].include?(handler.class) && !block_given? + raise ArgumentError, "#{self.class.name} for #{sym.inspect} has no valid handler (#{handler.inspect} was given)" + end + id = fromSymbol(sym) + @hash[id] = handler || handlerBlock if id + symbol = toSymbol(sym) + @hash[symbol] = handler || handlerBlock if symbol + end + + def copy(src,*dests) + handler = self[src] + if handler + for dest in dests + self.add(dest,handler) + end + end + end + + def [](sym) # 'sym' can be an ID or symbol + id = fromSymbol(sym) + ret = nil + ret = @hash[id] if id && @hash[id] # Real ID from the item + symbol = toSymbol(sym) + ret = @hash[symbol] if symbol && @hash[symbol] # Symbol or string + unless ret + for addif in @addIfs + return addif[1] if addif[0].call(id) + end + end + return ret + end + + def trigger(sym,*args) + handler = self[sym] + return (handler) ? handler.call(fromSymbol(sym),*args) : nil + end + + def clear + @hash.clear + end +end + +#=============================================================================== +# A stripped-down version of class HandlerHash which only deals with symbols and +# doesn't care about whether those symbols actually relate to a defined thing. +#=============================================================================== +class HandlerHash2 + def initialize + @hash = {} + @add_ifs = [] + end + + def [](sym) + sym = sym.id if !sym.is_a?(Symbol) && sym.respond_to?("id") + return @hash[sym] if sym && @hash[sym] + for add_if in @add_ifs + return add_if[1] if add_if[0].call(sym) + end + return nil + end + + def addIf(conditionProc, handler = nil, &handlerBlock) + if ![Proc, Hash].include?(handler.class) && !block_given? + raise ArgumentError, "addIf call for #{self.class.name} has no valid handler (#{handler.inspect} was given)" + end + @add_ifs.push([conditionProc, handler || handlerBlock]) + end + + def add(sym, handler = nil, &handlerBlock) + if ![Proc, Hash].include?(handler.class) && !block_given? + raise ArgumentError, "#{self.class.name} for #{sym.inspect} has no valid handler (#{handler.inspect} was given)" + end + @hash[sym] = handler || handlerBlock if sym + end + + def copy(src, *dests) + handler = self[src] + return if !handler + for dest in dests + self.add(dest, handler) + end + end + + def clear + @hash.clear + end + + def trigger(sym, *args) + sym = sym.id if !sym.is_a?(Symbol) && sym.respond_to?("id") + handler = self[sym] + return (handler) ? handler.call(sym, *args) : nil + end +end + +#=============================================================================== +# An even more stripped down version of class HandlerHash which just takes +# hashes with keys, no matter what the keys are. +#=============================================================================== +class HandlerHashBasic + def initialize + @ordered_keys = [] + @hash = {} + @addIfs = [] + end + + def [](entry) + ret = nil + ret = @hash[entry] if entry && @hash[entry] + unless ret + for addif in @addIfs + return addif[1] if addif[0].call(entry) + end + end + return ret + end + + def each + @ordered_keys.each { |key| yield key, @hash[key] } + end + + def add(entry, handler = nil, &handlerBlock) + if ![Proc,Hash].include?(handler.class) && !block_given? + raise ArgumentError, "#{self.class.name} for #{entry.inspect} has no valid handler (#{handler.inspect} was given)" + end + return if !entry || entry.empty? + @ordered_keys.push(entry) if !@ordered_keys.include?(entry) + @hash[entry] = handler || handlerBlock + end + + def addIf(conditionProc, handler = nil, &handlerBlock) + if ![Proc, Hash].include?(handler.class) && !block_given? + raise ArgumentError, "addIf call for #{self.class.name} has no valid handler (#{handler.inspect} was given)" + end + @addIfs.push([conditionProc, handler || handlerBlock]) + end + + def copy(src, *dests) + handler = self[src] + return if !handler + dests.each { |dest| self.add(dest, handler) } + end + + def clear + @hash.clear + @ordered_keys.clear + end + + def trigger(entry, *args) + handler = self[entry] + return (handler) ? handler.call(*args) : nil + end +end + +#=============================================================================== +# +#=============================================================================== +class SpeciesHandlerHash < HandlerHash2 +end + +class AbilityHandlerHash < HandlerHash2 +end + +class ItemHandlerHash < HandlerHash2 +end + +class MoveHandlerHash < HandlerHash2 +end diff --git a/Data/Scripts/003_Game processing/006_Event_OverworldEvents.rb b/Data/Scripts/003_Game processing/006_Event_OverworldEvents.rb new file mode 100644 index 000000000..b83ea0d6f --- /dev/null +++ b/Data/Scripts/003_Game processing/006_Event_OverworldEvents.rb @@ -0,0 +1,172 @@ +#=============================================================================== +# This module stores events that can happen during the game. A procedure can +# subscribe to an event by adding itself to the event. It will then be called +# whenever the event occurs. +#=============================================================================== +module Events + @@OnMapCreate = Event.new + @@OnMapUpdate = Event.new + @@OnMapChange = Event.new + @@OnMapChanging = Event.new + @@OnMapSceneChange = Event.new + @@OnSpritesetCreate = Event.new + @@OnAction = Event.new + @@OnStepTaken = Event.new + @@OnLeaveTile = Event.new + @@OnStepTakenFieldMovement = Event.new + @@OnStepTakenTransferPossible = Event.new + @@OnStartBattle = Event.new + @@OnEndBattle = Event.new + @@OnWildPokemonCreate = Event.new + @@OnWildBattleOverride = Event.new + @@OnWildBattleEnd = Event.new + @@OnTrainerPartyLoad = Event.new + @@OnChangeDirection = Event.new + + # Fires whenever a map is created. Event handler receives two parameters: the + # map (RPG::Map) and the tileset (RPG::Tileset) + def self.onMapCreate; @@OnMapCreate; end + def self.onMapCreate=(v); @@OnMapCreate = v; end + + # Fires each frame during a map update. + def self.onMapUpdate; @@OnMapUpdate; end + def self.onMapUpdate=(v); @@OnMapUpdate = v; end + + # Fires whenever one map is about to change to a different one. Event handler + # receives the new map ID and the Game_Map object representing the new map. + # When the event handler is called, $game_map still refers to the old map. + def self.onMapChanging; @@OnMapChanging; end + def self.onMapChanging=(v); @@OnMapChanging = v; end + + # Fires whenever the player moves to a new map. Event handler receives the old + # map ID or 0 if none. Also fires when the first map of the game is loaded + def self.onMapChange; @@OnMapChange; end + def self.onMapChange=(v); @@OnMapChange = v; end + + # Fires whenever the map scene is regenerated and soon after the player moves + # to a new map. + # Parameters: + # e[0] - Scene_Map object. + # e[1] - Whether the player just moved to a new map (either true or false). If + # false, some other code had called $scene.createSpritesets to + # regenerate the map scene without transferring the player elsewhere + def self.onMapSceneChange; @@OnMapSceneChange; end + def self.onMapSceneChange=(v); @@OnMapSceneChange = v; end + + # Fires whenever a spriteset is created. + # Parameters: + # e[0] - Spriteset being created. e[0].map is the map associated with the + # spriteset (not necessarily the current map). + # e[1] - Viewport used for tilemap and characters + def self.onSpritesetCreate; @@OnSpritesetCreate; end + def self.onSpritesetCreate=(v); @@OnSpritesetCreate = v; end + + # Triggers when the player presses the Action button on the map. + def self.onAction; @@OnAction; end + def self.onAction=(v); @@OnAction = v; end + + # Fires whenever the player takes a step. + def self.onStepTaken; @@OnStepTaken; end + def self.onStepTaken=(v); @@OnStepTaken = v; end + + # Fires whenever the player or another event leaves a tile. + # Parameters: + # e[0] - Event that just left the tile. + # e[1] - Map ID where the tile is located (not necessarily + # the current map). Use "$MapFactory.getMap(e[1])" to + # get the Game_Map object corresponding to that map. + # e[2] - X-coordinate of the tile + # e[3] - Y-coordinate of the tile + def self.onLeaveTile; @@OnLeaveTile; end + def self.onLeaveTile=(v); @@OnLeaveTile = v; end + + # Fires whenever the player or another event enters a tile. + # Parameters: + # e[0] - Event that just entered a tile. + def self.onStepTakenFieldMovement; @@OnStepTakenFieldMovement; end + def self.onStepTakenFieldMovement=(v); @@OnStepTakenFieldMovement = v; end + + # Fires whenever the player takes a step. The event handler may possibly move + # the player elsewhere. + # Parameters: + # e[0] - Array that contains a single boolean value. If an event handler moves + # the player to a new map, it should set this value to true. Other + # event handlers should check this parameter's value. + def self.onStepTakenTransferPossible; @@OnStepTakenTransferPossible; end + def self.onStepTakenTransferPossible=(v); @@OnStepTakenTransferPossible = v; end + + def self.onStartBattle; @@OnStartBattle; end + def self.onStartBattle=(v); @@OnStartBattle = v; end + + def self.onEndBattle; @@OnEndBattle; end + def self.onEndBattle=(v); @@OnEndBattle = v; end + + # Triggers whenever a wild Pokémon is created + # Parameters: + # e[0] - Pokémon being created + def self.onWildPokemonCreate; @@OnWildPokemonCreate; end + def self.onWildPokemonCreate=(v); @@OnWildPokemonCreate = v; end + + # Triggers at the start of a wild battle. Event handlers can provide their + # own wild battle routines to override the default behavior. + def self.onWildBattleOverride; @@OnWildBattleOverride; end + def self.onWildBattleOverride=(v); @@OnWildBattleOverride = v; end + + # Triggers whenever a wild Pokémon battle ends + # Parameters: + # e[0] - Pokémon species + # e[1] - Pokémon level + # e[2] - Battle result (1-win, 2-loss, 3-escaped, 4-caught, 5-draw) + def self.onWildBattleEnd; @@OnWildBattleEnd; end + def self.onWildBattleEnd=(v); @@OnWildBattleEnd = v; end + + # Triggers whenever an NPC trainer's Pokémon party is loaded + # Parameters: + # e[0] - Trainer + # e[1] - Items possessed by the trainer + # e[2] - Party + def self.onTrainerPartyLoad; @@OnTrainerPartyLoad; end + def self.onTrainerPartyLoad=(v); @@OnTrainerPartyLoad = v; end + + # Fires whenever the player changes direction. + def self.onChangeDirection; @@OnChangeDirection; end + def self.onChangeDirection=(v); @@OnChangeDirection = v; end +end + +#=============================================================================== +# +#=============================================================================== +def pbOnSpritesetCreate(spriteset,viewport) + Events.onSpritesetCreate.trigger(nil,spriteset,viewport) +end + +#=============================================================================== +# This module stores encounter-modifying events that can happen during the game. +# A procedure can subscribe to an event by adding itself to the event. It will +# then be called whenever the event occurs. +#=============================================================================== +module EncounterModifier + @@procs = [] + @@procsEnd = [] + + def self.register(p) + @@procs.push(p) + end + + def self.registerEncounterEnd(p) + @@procsEnd.push(p) + end + + def self.trigger(encounter) + for prc in @@procs + encounter = prc.call(encounter) + end + return encounter + end + + def self.triggerEncounterEnd() + for prc in @@procsEnd + prc.call() + end + end +end diff --git a/Data/Scripts/004_Game classes/001_Game_Screen.rb b/Data/Scripts/004_Game classes/001_Game_Screen.rb new file mode 100644 index 000000000..15733d2e6 --- /dev/null +++ b/Data/Scripts/004_Game classes/001_Game_Screen.rb @@ -0,0 +1,157 @@ +#=============================================================================== +# ** Game_Screen +#------------------------------------------------------------------------------- +# This class handles screen maintenance data, such as change in color tone, +# flashing, etc. Refer to "$game_screen" for the instance of this class. +#=============================================================================== + +class Game_Screen + #----------------------------------------------------------------------------- + # * Public Instance Variables + #----------------------------------------------------------------------------- + attr_reader :brightness # brightness + attr_reader :tone # color tone + attr_reader :flash_color # flash color + attr_reader :shake # shake positioning + attr_reader :pictures # pictures + attr_reader :weather_type # weather type + attr_reader :weather_max # max number of weather sprites + attr_accessor :weather_duration # ticks in which the weather should fade in + attr_accessor :weather_power + #----------------------------------------------------------------------------- + # * Object Initialization + #----------------------------------------------------------------------------- + def initialize + @brightness = 255 + @fadeout_duration = 0 + @fadein_duration = 0 + @tone = Tone.new(0, 0, 0, 0) + @tone_target = Tone.new(0, 0, 0, 0) + @tone_duration = 0 + @flash_color = Color.new(0, 0, 0, 0) + @flash_duration = 0 + @shake_power = 0 + @shake_speed = 0 + @shake_duration = 0 + @shake_direction = 1 + @shake = 0 + @pictures = [nil] + for i in 1..100 + @pictures.push(Game_Picture.new(i)) + end + @weather_type = 0 + @weather_max = 0.0 + @weather_duration = 0 + @weather_power = 0 + end + #----------------------------------------------------------------------------- + # * Start Changing Color Tone + # tone : color tone + # duration : time + #----------------------------------------------------------------------------- + def start_tone_change(tone, duration) + @tone_target = tone.clone + @tone_duration = duration + if @tone_duration == 0 + @tone = @tone_target.clone + end + end + #----------------------------------------------------------------------------- + # * Start Flashing + # color : color + # duration : time + #----------------------------------------------------------------------------- + def start_flash(color, duration) + @flash_color = color.clone + @flash_duration = duration + end + #----------------------------------------------------------------------------- + # * Start Shaking + # power : strength + # speed : speed + # duration : time + #----------------------------------------------------------------------------- + def start_shake(power, speed, duration) + @shake_power = power + @shake_speed = speed + @shake_duration = duration + end + #----------------------------------------------------------------------------- + # * Set Weather + # type : type + # power : strength + # duration : time + #----------------------------------------------------------------------------- + def weather(type, power, duration) + @weather_type = GameData::Weather.get(type).id + @weather_power = power + @weather_max = (power + 1) * RPG::Weather::MAX_SPRITES / 10 + @weather_duration = duration # In 1/20ths of a seconds + end + #----------------------------------------------------------------------------- + # * Frame Update + #----------------------------------------------------------------------------- + def update + if @fadeout_duration && @fadeout_duration>=1 + d = @fadeout_duration + @brightness = (@brightness*(d-1))/d + @fadeout_duration -= 1 + end + if @fadein_duration && @fadein_duration>=1 + d = @fadein_duration + @brightness = (@brightness*(d-1)+255)/d + @fadein_duration -= 1 + end + if @tone_duration>=1 + d = @tone_duration + @tone.red = (@tone.red*(d-1)+@tone_target.red)/d + @tone.green = (@tone.green*(d-1)+@tone_target.green)/d + @tone.blue = (@tone.blue*(d-1)+@tone_target.blue)/d + @tone.gray = (@tone.gray*(d-1)+@tone_target.gray)/d + @tone_duration -= 1 + end + if @flash_duration>=1 + d = @flash_duration + @flash_color.alpha = @flash_color.alpha*(d-1)/d + @flash_duration -= 1 + end + if @shake_duration>=1 || @shake!=0 + delta = (@shake_power*@shake_speed*@shake_direction)/10.0 + if @shake_duration<=1 && @shake*(@shake+delta)<0 + @shake = 0 + else + @shake += delta + end + @shake_direction = -1 if @shake>@shake_power*2 + @shake_direction = 1 if @shake<-@shake_power*2 + @shake_duration -= 1 if @shake_duration>=1 + end + if $game_temp.in_battle + for i in 51..100 + @pictures[i].update + end + else + for i in 1..50 + @pictures[i].update + end + end + end +end + +#=============================================================================== +# +#=============================================================================== +def pbToneChangeAll(tone,duration) + $game_screen.start_tone_change(tone,duration*Graphics.frame_rate/20) + for picture in $game_screen.pictures + picture.start_tone_change(tone,duration*Graphics.frame_rate/20) if picture + end +end + +def pbShake(power,speed,frames) + $game_screen.start_shake(power,speed,frames*Graphics.frame_rate/20) +end + +def pbFlash(color,frames) + $game_screen.start_flash(color,frames*Graphics.frame_rate/20) +end diff --git a/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb b/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb new file mode 100644 index 000000000..ac969e184 --- /dev/null +++ b/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb @@ -0,0 +1,68 @@ +#=============================================================================== +# ** Game_Temp +#------------------------------------------------------------------------------- +# This class handles temporary data that is not included with save data. +# Refer to "$game_temp" for the instance of this class. +#=============================================================================== +class Game_Temp + attr_accessor :message_window_showing # message window showing + attr_accessor :common_event_id # common event ID + attr_accessor :in_battle # in-battle flag + attr_accessor :battle_abort # battle flag: interrupt + attr_accessor :battleback_name # battleback file name + attr_accessor :in_menu # menu is open + attr_accessor :menu_beep # menu: play sound effect flag + attr_accessor :menu_calling # menu calling flag + attr_accessor :debug_calling # debug calling flag + attr_accessor :player_transferring # player place movement flag + attr_accessor :player_new_map_id # player destination: map ID + attr_accessor :player_new_x # player destination: x-coordinate + attr_accessor :player_new_y # player destination: y-coordinate + attr_accessor :player_new_direction # player destination: direction + attr_accessor :transition_processing # transition processing flag + attr_accessor :transition_name # transition file name + attr_accessor :to_title # return to title screen flag + attr_accessor :fadestate # for sprite hashes + attr_accessor :background_bitmap + attr_accessor :mart_prices + attr_accessor :unimportedSprites + attr_accessor :nb_imported_sprites + attr_accessor :loading_screen + attr_accessor :custom_sprites_list + attr_accessor :base_sprites_list + + #----------------------------------------------------------------------------- + # * Object Initialization + #----------------------------------------------------------------------------- + def initialize + @message_window_showing = false + @common_event_id = 0 + @in_battle = false + @battle_abort = false + @battleback_name = '' + @in_menu = false + @menu_beep = false + @menu_calling = false + @debug_calling = false + @player_transferring = false + @player_new_map_id = 0 + @player_new_x = 0 + @player_new_y = 0 + @player_new_direction = 0 + @transition_processing = false + @transition_name = "" + @to_title = false + @fadestate = 0 + @background_bitmap = nil + @message_window_showing = false + @transition_processing = false + @mart_prices = {} + @custom_sprites_list ={} + @base_sprites_list ={} + + end + + def clear_mart_prices + @mart_prices = {} + end +end diff --git a/Data/Scripts/004_Game classes/001_Switches and Variables/002_Game_Switches.rb b/Data/Scripts/004_Game classes/001_Switches and Variables/002_Game_Switches.rb new file mode 100644 index 000000000..5dc91702f --- /dev/null +++ b/Data/Scripts/004_Game classes/001_Switches and Variables/002_Game_Switches.rb @@ -0,0 +1,30 @@ +#=============================================================================== +# ** Game_Switches +#------------------------------------------------------------------------------- +# This class handles switches. It's a wrapper for the built-in class "Array." +# Refer to "$game_switches" for the instance of this class. +#=============================================================================== +class Game_Switches + #----------------------------------------------------------------------------- + # * Object Initialization + #----------------------------------------------------------------------------- + def initialize + @data = [] + end + #----------------------------------------------------------------------------- + # * Get Switch + # switch_id : switch ID + #----------------------------------------------------------------------------- + def [](switch_id) + return @data[switch_id] if switch_id <= 5000 && @data[switch_id] != nil + return false + end + #----------------------------------------------------------------------------- + # * Set Switch + # switch_id : switch ID + # value : ON (true) / OFF (false) + #----------------------------------------------------------------------------- + def []=(switch_id, value) + @data[switch_id] = value if switch_id <= 5000 + end +end diff --git a/Data/Scripts/004_Game classes/001_Switches and Variables/003_Game_Variables.rb b/Data/Scripts/004_Game classes/001_Switches and Variables/003_Game_Variables.rb new file mode 100644 index 000000000..70d16f90e --- /dev/null +++ b/Data/Scripts/004_Game classes/001_Switches and Variables/003_Game_Variables.rb @@ -0,0 +1,30 @@ +#=============================================================================== +# ** Game_Variables +#------------------------------------------------------------------------------- +# This class handles variables. It's a wrapper for the built-in class "Array." +# Refer to "$game_variables" for the instance of this class. +#=============================================================================== +class Game_Variables + #----------------------------------------------------------------------------- + # * Object Initialization + #----------------------------------------------------------------------------- + def initialize + @data = [] + end + #----------------------------------------------------------------------------- + # * Get Variable + # variable_id : variable ID + #----------------------------------------------------------------------------- + def [](variable_id) + return @data[variable_id] if variable_id <= 5000 && !@data[variable_id].nil? + return 0 + end + #----------------------------------------------------------------------------- + # * Set Variable + # variable_id : variable ID + # value : the variable's value + #----------------------------------------------------------------------------- + def []=(variable_id, value) + @data[variable_id] = value if variable_id <= 5000 + end +end diff --git a/Data/Scripts/004_Game classes/001_Switches and Variables/004_Game_SelfSwitches.rb b/Data/Scripts/004_Game classes/001_Switches and Variables/004_Game_SelfSwitches.rb new file mode 100644 index 000000000..528e4b1c0 --- /dev/null +++ b/Data/Scripts/004_Game classes/001_Switches and Variables/004_Game_SelfSwitches.rb @@ -0,0 +1,29 @@ +#=============================================================================== +# ** Game_SelfSwitches +#------------------------------------------------------------------------------- +# This class handles self switches. It's a wrapper for the built-in class +# "Hash." Refer to "$game_self_switches" for the instance of this class. +#=============================================================================== +class Game_SelfSwitches + #----------------------------------------------------------------------------- + # * Object Initialization + #----------------------------------------------------------------------------- + def initialize + @data = {} + end + #----------------------------------------------------------------------------- + # * Get Self Switch + # key : key + #----------------------------------------------------------------------------- + def [](key) + return (@data[key]==true) ? true : false + end + #----------------------------------------------------------------------------- + # * Set Self Switch + # key : key + # value : ON (true) / OFF (false) + #----------------------------------------------------------------------------- + def []=(key, value) + @data[key] = value + end +end diff --git a/Data/Scripts/004_Game classes/002_Game_System.rb b/Data/Scripts/004_Game classes/002_Game_System.rb new file mode 100644 index 000000000..7db54145f --- /dev/null +++ b/Data/Scripts/004_Game classes/002_Game_System.rb @@ -0,0 +1,286 @@ +#============================================================================== +# ** Game_System +#------------------------------------------------------------------------------ +# This class handles data surrounding the system. Backround music, etc. +# is managed here as well. Refer to "$game_system" for the instance of +# this class. +#============================================================================== +class Game_System + attr_reader :map_interpreter # map event interpreter + attr_reader :battle_interpreter # battle event interpreter + attr_accessor :timer # timer + attr_accessor :timer_working # timer working flag + attr_accessor :save_disabled # save forbidden + attr_accessor :menu_disabled # menu forbidden + attr_accessor :encounter_disabled # encounter forbidden + attr_accessor :message_position # text option: positioning + attr_accessor :message_frame # text option: window frame + attr_accessor :save_count # save count + attr_accessor :magic_number # magic number + attr_accessor :autoscroll_x_speed + attr_accessor :autoscroll_y_speed + attr_accessor :bgm_position + + def initialize + @map_interpreter = Interpreter.new(0, true) + @battle_interpreter = Interpreter.new(0, false) + @timer = 0 + @timer_working = false + @save_disabled = false + @menu_disabled = false + @encounter_disabled = false + @message_position = 2 + @message_frame = 0 + @save_count = 0 + @magic_number = 0 + @autoscroll_x_speed = 0 + @autoscroll_y_speed = 0 + @bgm_position = 0 + @bgs_position = 0 + end + +################################################################################ + + def bgm_play(bgm) + old_pos = @bgm_position + @bgm_position = 0 + bgm_play_internal(bgm,0) + @bgm_position = old_pos + end + + def bgm_play_internal2(name,volume,pitch,position) # :nodoc: + vol = volume + vol *= $PokemonSystem.bgmvolume/100.0 + vol = vol.to_i + begin + Audio.bgm_play(name,vol,pitch,position) + rescue ArgumentError + Audio.bgm_play(name,vol,pitch) + end + end + + def bgm_play_internal(bgm,position) # :nodoc: + @bgm_position = position if !@bgm_paused + @playing_bgm = (bgm==nil) ? nil : bgm.clone + if bgm!=nil && bgm.name!="" + if FileTest.audio_exist?("Audio/BGM/"+bgm.name) + bgm_play_internal2("Audio/BGM/"+bgm.name, + bgm.volume,bgm.pitch,@bgm_position) if !@defaultBGM + end + else + @bgm_position = position if !@bgm_paused + @playing_bgm = nil + Audio.bgm_stop if !@defaultBGM + end + if @defaultBGM + bgm_play_internal2("Audio/BGM/"+@defaultBGM.name, + @defaultBGM.volume,@defaultBGM.pitch,@bgm_position) + end + Graphics.frame_reset + end + + def bgm_pause(fadetime=0.0) # :nodoc: + pos = Audio.bgm_pos rescue 0 + self.bgm_fade(fadetime) if fadetime>0.0 + @bgm_position = pos + @bgm_paused = true + end + + def bgm_unpause # :nodoc: + @bgm_position = 0 + @bgm_paused = false + end + + def bgm_resume(bgm) # :nodoc: + if @bgm_paused + self.bgm_play_internal(bgm,@bgm_position) + @bgm_position = 0 + @bgm_paused = false + end + end + + def bgm_stop # :nodoc: + @bgm_position = 0 if !@bgm_paused + @playing_bgm = nil + Audio.bgm_stop if !@defaultBGM + end + + def bgm_fade(time) # :nodoc: + @bgm_position = 0 if !@bgm_paused + @playing_bgm = nil + Audio.bgm_fade((time*1000).floor) if !@defaultBGM + end + + def playing_bgm + return @playing_bgm + end + + # Saves the currently playing background music for later playback. + def bgm_memorize + @memorized_bgm = @playing_bgm + end + + # Plays the currently memorized background music + def bgm_restore + bgm_play(@memorized_bgm) + end + + # Returns an RPG::AudioFile object for the currently playing background music + def getPlayingBGM + return (@playing_bgm) ? @playing_bgm.clone : nil + end + + def setDefaultBGM(bgm,volume=80,pitch=100) + bgm = RPG::AudioFile.new(bgm,volume,pitch) if bgm.is_a?(String) + if bgm!=nil && bgm.name!="" + @defaultBGM = nil + self.bgm_play(bgm) + @defaultBGM = bgm.clone + else + @defaultBGM = nil + self.bgm_play(@playing_bgm) + end + end + +################################################################################ + + def me_play(me) + me = RPG::AudioFile.new(me) if me.is_a?(String) + if me!=nil && me.name!="" + if FileTest.audio_exist?("Audio/ME/"+me.name) + vol = me.volume + vol *= $PokemonSystem.bgmvolume/100.0 + vol = vol.to_i + Audio.me_play("Audio/ME/"+me.name,vol,me.pitch) + end + else + Audio.me_stop + end + Graphics.frame_reset + end + +################################################################################ + + def bgs_play(bgs) + @playing_bgs = (bgs==nil) ? nil : bgs.clone + if bgs!=nil && bgs.name!="" + if FileTest.audio_exist?("Audio/BGS/"+bgs.name) + vol = bgs.volume + vol *= $PokemonSystem.sevolume/100.0 + vol = vol.to_i + Audio.bgs_play("Audio/BGS/"+bgs.name,vol,bgs.pitch) + end + else + @bgs_position = 0 + @playing_bgs = nil + Audio.bgs_stop + end + Graphics.frame_reset + end + + def bgs_pause(fadetime=0.0) # :nodoc: + if fadetime>0.0 + self.bgs_fade(fadetime) + else + self.bgs_stop + end + @bgs_paused = true + end + + def bgs_unpause # :nodoc: + @bgs_paused = false + end + + def bgs_resume(bgs) # :nodoc: + if @bgs_paused + self.bgs_play(bgs) + @bgs_paused = false + end + end + + def bgs_stop + @bgs_position = 0 + @playing_bgs = nil + Audio.bgs_stop + end + + def bgs_fade(time) + @bgs_position = 0 + @playing_bgs = nil + Audio.bgs_fade((time*1000).floor) + end + + def playing_bgs + return @playing_bgs + end + + def bgs_memorize + @memorized_bgs = @playing_bgs + end + + def bgs_restore + bgs_play(@memorized_bgs) + end + + def getPlayingBGS + return (@playing_bgs) ? @playing_bgs.clone : nil + end + +################################################################################ + + def se_play(se) + se = RPG::AudioFile.new(se) if se.is_a?(String) + if se!=nil && se.name!="" && FileTest.audio_exist?("Audio/SE/"+se.name) + vol = se.volume + vol *= $PokemonSystem.sevolume/100.0 + vol = vol.to_i + Audio.se_play("Audio/SE/"+se.name,vol,se.pitch) + end + end + + def se_stop + Audio.se_stop + end + +################################################################################ + + def battle_bgm + return (@battle_bgm) ? @battle_bgm : $data_system.battle_bgm + end + + def battle_bgm=(battle_bgm) + @battle_bgm = battle_bgm + end + + def battle_end_me + return (@battle_end_me) ? @battle_end_me : $data_system.battle_end_me + end + + def battle_end_me=(battle_end_me) + @battle_end_me = battle_end_me + end + +################################################################################ + + def windowskin_name + if @windowskin_name==nil + return $data_system.windowskin_name + else + return @windowskin_name + end + end + + def windowskin_name=(windowskin_name) + @windowskin_name = windowskin_name + end + + def update + @timer -= 1 if @timer_working && @timer>0 + if Input.trigger?(Input::SPECIAL) && pbCurrentEventCommentInput(1,"Cut Scene") + event = @map_interpreter.get_character(0) + @map_interpreter.pbSetSelfSwitch(event.id,"A",true) + @map_interpreter.command_end + event.start + end + end +end diff --git a/Data/Scripts/004_Game classes/003_Game_Picture.rb b/Data/Scripts/004_Game classes/003_Game_Picture.rb new file mode 100644 index 000000000..5d5be5b44 --- /dev/null +++ b/Data/Scripts/004_Game classes/003_Game_Picture.rb @@ -0,0 +1,156 @@ +#=============================================================================== +# ** Game_Picture +#------------------------------------------------------------------------------- +# This class handles the picture. It's used within the Game_Screen class +# ($game_screen). +#=============================================================================== + +class Game_Picture + #----------------------------------------------------------------------------- + # * Public Instance Variables + #----------------------------------------------------------------------------- + attr_reader :number # picture number + attr_accessor :name # file name + attr_reader :origin # starting point + attr_reader :x # x-coordinate + attr_reader :y # y-coordinate + attr_accessor :zoom_x # x directional zoom rate + attr_accessor :zoom_y # y directional zoom rate + attr_accessor :opacity # opacity level + attr_reader :blend_type # blend method + attr_reader :tone # color tone + attr_reader :angle # rotation angle + #----------------------------------------------------------------------------- + # * Object Initialization + # number : picture number + #----------------------------------------------------------------------------- + def initialize(number) + @number = number + @name = "" + @origin = 0 + @x = 0.0 + @y = 0.0 + @zoom_x = 100.0 + @zoom_y = 100.0 + @opacity = 255.0 + @blend_type = 1 + @duration = 0 + @target_x = @x + @target_y = @y + @target_zoom_x = @zoom_x + @target_zoom_y = @zoom_y + @target_opacity = @opacity + @tone = Tone.new(0, 0, 0, 0) + @tone_target = Tone.new(0, 0, 0, 0) + @tone_duration = 0 + @angle = 0 + @rotate_speed = 0 + end + #----------------------------------------------------------------------------- + # * Show Picture + # name : file name + # origin : starting point + # x : x-coordinate + # y : y-coordinate + # zoom_x : x directional zoom rate + # zoom_y : y directional zoom rate + # opacity : opacity level + # blend_type : blend method + #----------------------------------------------------------------------------- + def show(name, origin, x, y, zoom_x=100, zoom_y=100, opacity=255, blend_type=0) + @name = name + @origin = origin + @x = x.to_f + @y = y.to_f + @zoom_x = zoom_x.to_f + @zoom_y = zoom_y.to_f + @opacity = opacity.to_f + @blend_type = blend_type ? blend_type : 0 + @duration = 0 + @target_x = @x + @target_y = @y + @target_zoom_x = @zoom_x + @target_zoom_y = @zoom_y + @target_opacity = @opacity + @tone = Tone.new(0, 0, 0, 0) + @tone_target = Tone.new(0, 0, 0, 0) + @tone_duration = 0 + @angle = 0 + @rotate_speed = 0 + end + #----------------------------------------------------------------------------- + # * Move Picture + # duration : time + # origin : starting point + # x : x-coordinate + # y : y-coordinate + # zoom_x : x directional zoom rate + # zoom_y : y directional zoom rate + # opacity : opacity level + # blend_type : blend method + #----------------------------------------------------------------------------- + def move(duration, origin, x, y, zoom_x, zoom_y, opacity, blend_type) + @duration = duration + @origin = origin + @target_x = x.to_f + @target_y = y.to_f + @target_zoom_x = zoom_x.to_f + @target_zoom_y = zoom_y.to_f + @target_opacity = opacity.to_f + @blend_type = blend_type ? blend_type : 0 + end + #----------------------------------------------------------------------------- + # * Change Rotation Speed + # speed : rotation speed + #----------------------------------------------------------------------------- + def rotate(speed) + @rotate_speed = speed + end + #----------------------------------------------------------------------------- + # * Start Change of Color Tone + # tone : color tone + # duration : time + #----------------------------------------------------------------------------- + def start_tone_change(tone, duration) + @tone_target = tone.clone + @tone_duration = duration + if @tone_duration == 0 + @tone = @tone_target.clone + end + end + #----------------------------------------------------------------------------- + # * Erase Picture + #----------------------------------------------------------------------------- + def erase + @name = "" + end + #----------------------------------------------------------------------------- + # * Frame Update + #----------------------------------------------------------------------------- + def update + if @duration >= 1 + d = @duration + @x = (@x * (d - 1) + @target_x) / d + @y = (@y * (d - 1) + @target_y) / d + @zoom_x = (@zoom_x * (d - 1) + @target_zoom_x) / d + @zoom_y = (@zoom_y * (d - 1) + @target_zoom_y) / d + @opacity = (@opacity * (d - 1) + @target_opacity) / d + @duration -= 1 + end + if @tone_duration >= 1 + d = @tone_duration + @tone.red = (@tone.red * (d - 1) + @tone_target.red) / d + @tone.green = (@tone.green * (d - 1) + @tone_target.green) / d + @tone.blue = (@tone.blue * (d - 1) + @tone_target.blue) / d + @tone.gray = (@tone.gray * (d - 1) + @tone_target.gray) / d + @tone_duration -= 1 + end + if @rotate_speed != 0 + @angle += @rotate_speed / 2.0 + while @angle < 0 + @angle += 360 + end + @angle %= 360 + end + end +end diff --git a/Data/Scripts/004_Game classes/004_Game_Map.rb b/Data/Scripts/004_Game classes/004_Game_Map.rb new file mode 100644 index 000000000..cdcbb68a4 --- /dev/null +++ b/Data/Scripts/004_Game classes/004_Game_Map.rb @@ -0,0 +1,547 @@ +#============================================================================== +# ** Game_Map +#------------------------------------------------------------------------------ +# This class handles the map. It includes scrolling and passable determining +# functions. Refer to "$game_map" for the instance of this class. +#============================================================================== +class Game_Map + attr_accessor :map_id + attr_accessor :tileset_name # tileset file name + attr_accessor :autotile_names # autotile file name + attr_reader :passages # passage table + attr_reader :priorities # priority table + attr_reader :terrain_tags # terrain tag table + attr_reader :events # events + attr_accessor :panorama_name # panorama file name + attr_accessor :panorama_hue # panorama hue + attr_accessor :fog_name # fog file name + attr_accessor :fog_hue # fog hue + attr_accessor :fog_opacity # fog opacity level + attr_accessor :fog_blend_type # fog blending method + attr_accessor :fog_zoom # fog zoom rate + + attr_accessor :fog_sx # fog sx + attr_accessor :fog_sy # fog sy + attr_accessor :fog_ox # fog x-coordinate starting point + attr_accessor :fog_oy # fog y-coordinate starting point + + attr_accessor :fog2_ox # fog x-coordinate starting point + attr_accessor :fog2_oy # fog y-coordinate starting point + attr_accessor :fog2_sx # fog sx + attr_accessor :fog2_sy # fog sy + attr_accessor :fog2_opacity # fog sy + + attr_reader :fog_tone # fog color tone + attr_accessor :battleback_name # battleback file name + attr_reader :display_x # display x-coordinate * 128 + attr_reader :display_y # display y-coordinate * 128 + attr_accessor :need_refresh # refresh request flag + attr_accessor :scroll_direction + + TILE_WIDTH = 32 + TILE_HEIGHT = 32 + X_SUBPIXELS = 4 + Y_SUBPIXELS = 4 + REAL_RES_X = TILE_WIDTH * X_SUBPIXELS + REAL_RES_Y = TILE_HEIGHT * Y_SUBPIXELS + + def initialize + @map_id = 0 + @display_x = 0 + @display_y = 0 + end + + def setup(map_id) + @map_id = map_id + @map = load_data(sprintf("Data/Map%03d.rxdata", map_id)) + tileset = $data_tilesets[@map.tileset_id] + updateTileset + @fog_ox = 0 + @fog_oy = 0 + + @fog2_ox = 0 + @fog2_oy = 0 + @fog2_sx = 0 + @fog2_sy = 0 + @fog2_opacity = 0 + + @fog_tone = Tone.new(0, 0, 0, 0) + @fog_tone_target = Tone.new(0, 0, 0, 0) + @fog_tone_duration = 0 + @fog_opacity_duration = 0 + @fog_opacity_target = 0 + self.display_x = 0 + self.display_y = 0 + @need_refresh = false + Events.onMapCreate.trigger(self, map_id, @map, tileset) + @events = {} + for i in @map.events.keys + @events[i] = Game_Event.new(@map_id, @map.events[i], self) + end + @common_events = {} + for i in 1...$data_common_events.size + @common_events[i] = Game_CommonEvent.new(i) + end + @scroll_direction = 2 + @scroll_rest = 0 + @scroll_speed = 4 + end + + def updateTileset + tileset = $data_tilesets[@map.tileset_id] + @tileset_name = tileset.tileset_name + @autotile_names = tileset.autotile_names + @panorama_name = tileset.panorama_name + @panorama_hue = tileset.panorama_hue + @fog_name = tileset.fog_name + @fog_hue = tileset.fog_hue + @fog_opacity = tileset.fog_opacity + @fog_blend_type = tileset.fog_blend_type + @fog_zoom = tileset.fog_zoom + @fog_sx = tileset.fog_sx + @fog_sy = tileset.fog_sy + @battleback_name = tileset.battleback_name + @passages = tileset.passages + @priorities = tileset.priorities + @terrain_tags = tileset.terrain_tags + end + + def width + return @map.width; + end + + def height + return @map.height; + end + + def encounter_list + return @map.encounter_list; + end + + def encounter_step + return @map.encounter_step; + end + + def data + return @map.data; + end + + def tileset_id; + return @map.tileset_id; + end + + def name + ret = pbGetMessage(MessageTypes::MapNames, @map_id) + ret.gsub!(/\\PN/, $Trainer.name) if $Trainer + return ret + end + + #----------------------------------------------------------------------------- + # * Autoplays background music + # Plays music called "[normal BGM]_n" if it's night time and it exists + #----------------------------------------------------------------------------- + def autoplayAsCue + if @map.autoplay_bgm + if PBDayNight.isNight? && FileTest.audio_exist?("Audio/BGM/" + @map.bgm.name + "_n") + pbCueBGM(@map.bgm.name + "_n", 1.0, @map.bgm.volume, @map.bgm.pitch) + else + pbCueBGM(@map.bgm, 1.0) + end + end + if @map.autoplay_bgs + pbBGSPlay(@map.bgs) + end + end + + def setFog2(filename,sx=0,sy=0,opacity=32) + @fog2_sx=sx + @fog2_sy=-sy + @fog2_opacity = opacity + $scene.spriteset.setFog2(filename) + end + + def eraseFog2() + @fog2_sx=0 + @fog2_sy=-0 + @fog2_opacity = 0 + $scene.spriteset.disposeFog2() + end + + + #----------------------------------------------------------------------------- + # * Plays background music + # Plays music called "[normal BGM]_n" if it's night time and it exists + #----------------------------------------------------------------------------- + def autoplay + if @map.autoplay_bgm + if PBDayNight.isNight? && FileTest.audio_exist?("Audio/BGM/" + @map.bgm.name + "_n") + pbBGMPlay(@map.bgm.name + "_n", @map.bgm.volume, @map.bgm.pitch) + else + pbBGMPlay(@map.bgm) + end + end + if @map.autoplay_bgs + pbBGSPlay(@map.bgs) + end + end + + def valid?(x, y) + return x >= 0 && x < width && y >= 0 && y < height + end + + def validLax?(x, y) + return x >= -10 && x <= width + 10 && y >= -10 && y <= height + 10 + end + + def passable?(x, y, d, self_event = nil) + return false if !valid?(x, y) + bit = (1 << (d / 2 - 1)) & 0x0f + for event in events.values + next if event.tile_id <= 0 + next if event == self_event + next if !event.at_coordinate?(x, y) + next if event.through + next if GameData::TerrainTag.try_get(@terrain_tags[event.tile_id]).ignore_passability + passage = @passages[event.tile_id] + return false if passage & bit != 0 + return false if passage & 0x0f == 0x0f + return true if @priorities[event.tile_id] == 0 + end + return playerPassable?(x, y, d, self_event) if self_event == $game_player + # All other events + newx = x + newy = y + case d + when 1 + newx -= 1 + newy += 1 + when 2 + newy += 1 + when 3 + newx += 1 + newy += 1 + when 4 + newx -= 1 + when 6 + newx += 1 + when 7 + newx -= 1 + newy -= 1 + when 8 + newy -= 1 + when 9 + newx += 1 + newy -= 1 + end + return false if !valid?(newx, newy) + for i in [2, 1, 0] + tile_id = data[x, y, i] + terrain = GameData::TerrainTag.try_get(@terrain_tags[tile_id]) + # If already on water, only allow movement to another water tile + if self_event != nil && terrain.can_surf_freely + for j in [2, 1, 0] + facing_tile_id = data[newx, newy, j] + return false if facing_tile_id == nil + facing_terrain = GameData::TerrainTag.try_get(@terrain_tags[facing_tile_id]) + if facing_terrain.id != :None && !facing_terrain.ignore_passability + return facing_terrain.can_surf_freely + end + end + return false + # Can't walk onto ice + # removed for mahogany gym. idk if this will cause problems, hopefully not + # elsif terrain.ice + # return false + elsif self_event != nil && self_event.x == x && self_event.y == y + # Can't walk onto ledges + for j in [2, 1, 0] + facing_tile_id = data[newx, newy, j] + return false if facing_tile_id == nil + facing_terrain = GameData::TerrainTag.try_get(@terrain_tags[facing_tile_id]) + return false if facing_terrain.ledge + break if facing_terrain.id != :None && !facing_terrain.ignore_passability + end + end + # Regular passability checks + if !terrain || !terrain.ignore_passability + passage = @passages[tile_id] + return false if passage & bit != 0 || passage & 0x0f == 0x0f + return true if @priorities[tile_id] == 0 + end + end + return true + end + + def playerPassable?(x, y, d, self_event = nil) + bit = (1 << (d / 2 - 1)) & 0x0f + for i in [2, 1, 0] + tile_id = data[x, y, i] + terrain = GameData::TerrainTag.try_get(@terrain_tags[tile_id]) + passage = @passages[tile_id] + if terrain + # Ignore bridge tiles if not on a bridge + next if terrain.bridge && $PokemonGlobal.bridge == 0 + # Make water tiles passable if player is surfing + return true if $PokemonGlobal.surfing && terrain.can_surf && !terrain.waterfall + # Prevent cycling in really tall grass/on ice + return false if $PokemonGlobal.bicycle && terrain.must_walk + # Depend on passability of bridge tile if on bridge + if terrain.bridge && $PokemonGlobal.bridge > 0 + return (passage & bit == 0 && passage & 0x0f != 0x0f) + end + end + # Regular passability checks + if !terrain || !terrain.ignore_passability + return false if passage & bit != 0 || passage & 0x0f == 0x0f + return true if @priorities[tile_id] == 0 + end + end + return true + end + + # Returns whether the position x,y is fully passable (there is no blocking + # event there, and the tile is fully passable in all directions) + def passableStrict?(x, y, d, self_event = nil) + return false if !valid?(x, y) + for event in events.values + next if event == self_event || event.tile_id < 0 || event.through + next if !event.at_coordinate?(x, y) + next if GameData::TerrainTag.try_get(@terrain_tags[event.tile_id]).ignore_passability + return false if @passages[event.tile_id] & 0x0f != 0 + return true if @priorities[event.tile_id] == 0 + end + for i in [2, 1, 0] + tile_id = data[x, y, i] + next if GameData::TerrainTag.try_get(@terrain_tags[tile_id]).ignore_passability + return false if @passages[tile_id] & 0x0f != 0 + return true if @priorities[tile_id] == 0 + end + return true + end + + def bush?(x, y) + for i in [2, 1, 0] + tile_id = data[x, y, i] + return false if GameData::TerrainTag.try_get(@terrain_tags[tile_id]).bridge && + $PokemonGlobal.bridge > 0 + return true if @passages[tile_id] & 0x40 == 0x40 + end + return false + end + + def deepBush?(x, y) + for i in [2, 1, 0] + tile_id = data[x, y, i] + terrain = GameData::TerrainTag.try_get(@terrain_tags[tile_id]) + return false if terrain.bridge && $PokemonGlobal.bridge > 0 + return true if terrain.deep_bush && @passages[tile_id] & 0x40 == 0x40 + end + return false + end + + def counter?(x, y) + for i in [2, 1, 0] + tile_id = data[x, y, i] + passage = @passages[tile_id] + return true if passage & 0x80 == 0x80 + end + return false + end + + def terrain_tag(x, y, countBridge = false) + if valid?(x, y) + for i in [2, 1, 0] + tile_id = data[x, y, i] + terrain = GameData::TerrainTag.try_get(@terrain_tags[tile_id]) + next if terrain.id == :None || terrain.ignore_passability + next if !countBridge && terrain.bridge && $PokemonGlobal.bridge == 0 + return terrain + end + end + return GameData::TerrainTag.get(:None) + end + + # Unused. + def check_event(x, y) + for event in self.events.values + return event.id if event.at_coordinate?(x, y) + end + end + + def event_at_position(x, y) + for event in self.events.values + return true if event.at_coordinate?(x, y) + end + return false + end + + def get_event_at_position(x, y, excluding_IDs = []) + for event in self.events.values + next if excluding_IDs.include?(event.id) + return event if event.at_coordinate?(x, y) + end + return nil + end + + def display_x=(value) + return if @display_x == value + @display_x = value + if GameData::MapMetadata.exists?(self.map_id) && GameData::MapMetadata.get(self.map_id).snap_edges + max_x = (self.width - Graphics.width * 1.0 / TILE_WIDTH) * REAL_RES_X + @display_x = [0, [@display_x, max_x].min].max + end + $MapFactory.setMapsInRange if $MapFactory + end + + def display_y=(value) + return if @display_y == value + @display_y = value + if GameData::MapMetadata.exists?(self.map_id) && GameData::MapMetadata.get(self.map_id).snap_edges + max_y = (self.height - Graphics.height * 1.0 / TILE_HEIGHT) * REAL_RES_Y + @display_y = [0, [@display_y, max_y].min].max + end + $MapFactory.setMapsInRange if $MapFactory + end + + def scroll_up(distance) + self.display_y -= distance + end + + def scroll_down(distance) + self.display_y += distance + end + + def scroll_left(distance) + self.display_x -= distance + end + + def scroll_right(distance) + self.display_x += distance + end + + def start_scroll(direction, distance, speed) + @scroll_direction = direction + if direction == 2 || direction == 8 # down or up + @scroll_rest = distance * REAL_RES_Y + else + @scroll_rest = distance * REAL_RES_X + end + @scroll_speed = speed + end + + def scrolling? + return @scroll_rest > 0 + end + + def start_fog_tone_change(tone, duration) + @fog_tone_target = tone.clone + @fog_tone_duration = duration + if @fog_tone_duration == 0 + @fog_tone = @fog_tone_target.clone + end + end + + def start_fog_opacity_change(opacity, duration) + @fog_opacity_target = opacity * 1.0 + @fog_opacity_duration = duration + if @fog_opacity_duration == 0 + @fog_opacity = @fog_opacity_target + end + end + + def refresh + for event in @events.values + event.refresh + end + for common_event in @common_events.values + common_event.refresh + end + @need_refresh = false + end + + def update + # refresh maps if necessary + if $MapFactory + for i in $MapFactory.maps + i.refresh if i.need_refresh + end + $MapFactory.setCurrentMap + end + # If scrolling + if @scroll_rest > 0 + distance = (1 << @scroll_speed) * 40.0 / Graphics.frame_rate + distance = @scroll_rest if distance > @scroll_rest + case @scroll_direction + when 2 then + scroll_down(distance) + when 4 then + scroll_left(distance) + when 6 then + scroll_right(distance) + when 8 then + scroll_up(distance) + end + @scroll_rest -= distance + end + # Only update events that are on-screen + for event in @events.values + event.update + end + # Update common events + for common_event in @common_events.values + common_event.update + end + # Update fog + @fog_ox -= @fog_sx / 8.0 + @fog_oy -= @fog_sy / 8.0 + + @fog2_ox -= @fog2_sx / 8.0 if @fog2_ox + @fog2_oy -= @fog2_sy / 8.0 if @fog2_oy + + if @fog_tone_duration >= 1 + d = @fog_tone_duration + target = @fog_tone_target + @fog_tone.red = (@fog_tone.red * (d - 1) + target.red) / d + @fog_tone.green = (@fog_tone.green * (d - 1) + target.green) / d + @fog_tone.blue = (@fog_tone.blue * (d - 1) + target.blue) / d + @fog_tone.gray = (@fog_tone.gray * (d - 1) + target.gray) / d + @fog_tone_duration -= 1 + end + if @fog_opacity_duration >= 1 + d = @fog_opacity_duration + @fog_opacity = (@fog_opacity * (d - 1) + @fog_opacity_target) / d + @fog_opacity_duration -= 1 + end + end +end + +#=============================================================================== +# +#=============================================================================== +def pbScrollMap(direction, distance, speed) + if speed == 0 + case direction + when 2 then + $game_map.scroll_down(distance * Game_Map::REAL_RES_Y) + when 4 then + $game_map.scroll_left(distance * Game_Map::REAL_RES_X) + when 6 then + $game_map.scroll_right(distance * Game_Map::REAL_RES_X) + when 8 then + $game_map.scroll_up(distance * Game_Map::REAL_RES_Y) + end + else + $game_map.start_scroll(direction, distance, speed) + oldx = $game_map.display_x + oldy = $game_map.display_y + loop do + Graphics.update + Input.update + break if !$game_map.scrolling? + pbUpdateSceneMap + break if $game_map.display_x == oldx && $game_map.display_y == oldy + oldx = $game_map.display_x + oldy = $game_map.display_y + end + end +end diff --git a/Data/Scripts/004_Game classes/005_Game_Map_Autoscroll.rb b/Data/Scripts/004_Game classes/005_Game_Map_Autoscroll.rb new file mode 100644 index 000000000..3af3ea189 --- /dev/null +++ b/Data/Scripts/004_Game classes/005_Game_Map_Autoscroll.rb @@ -0,0 +1,194 @@ +#=============================================================================== +# ** Map Autoscroll +#------------------------------------------------------------------------------- +# Wachunga +# Version 1.02 +# 2005-12-18 +#=============================================================================== +=begin + + This script supplements the built-in "Scroll Map" event command with the + aim of simplifying cutscenes (and map scrolling in general). Whereas the + normal event command requires a direction and number of tiles to scroll, + Map Autoscroll scrolls the map to center on the tile whose x and y + coordinates are given. + + FEATURES + - automatic map scrolling to given x,y coordinate (or player) + - destination is fixed, so it's possible to scroll to same place even if + origin is variable (e.g. moving NPC) + - variable speed (just like "Scroll Map" event command) + - diagonal scrolling supported + + SETUP + Instead of a "Scroll Map" event command, use the "Call Script" command + and enter on the following on the first line: + + autoscroll(x,y) + + (replacing "x" and "y" with the x and y coordinates of the tile to scroll to) + + To specify a scroll speed other than the default (4), use: + + autoscroll(x,y,speed) + + (now also replacing "speed" with the scroll speed from 1-6) + + Diagonal scrolling happens automatically when the destination is diagonal + relative to the starting point (i.e., not directly up, down, left or right). + + To scroll to the player, instead use the following: + + autoscroll_player(speed) + + Note: because of how the interpreter and the "Call Script" event command + are setup, the call to autoscroll(...) can only be on the first line of + the "Call Script" event command (and not flowing down to subsequent lines). + + For example, the following call may not work as expected: + + autoscroll($game_variables[1], + $game_variables[2]) + + (since the long argument names require dropping down to a second line) + A work-around is to setup new variables with shorter names in a preceding + (separate) "Call Script" event command: + + @x = $game_variables[1] + @y = $game_variables[2] + + and then use those as arguments: + + autoscroll(@x,@y) + + The renaming must be in a separate "Call Script" because otherwise + the call to autoscroll(...) isn't on the first line. + + Originally requested by militantmilo80: + http://www.rmxp.net/forums/index.php?showtopic=29519 + +=end + +class Interpreter + SCROLL_SPEED_DEFAULT = 4 + #----------------------------------------------------------------------------- + # * Map Autoscroll to Coordinates + # x : x coordinate to scroll to and center on + # y : y coordinate to scroll to and center on + # speed : (optional) scroll speed (from 1-6, default being 4) + #----------------------------------------------------------------------------- + def autoscroll(x,y,speed=SCROLL_SPEED_DEFAULT) + if $game_map.scrolling? + return false + elsif !$game_map.valid?(x,y) + print 'Map Autoscroll: given x,y is invalid' + return command_skip + elsif !(1..6).include?(speed) + print 'Map Autoscroll: invalid speed (1-6 only)' + return command_skip + end + center_x = (Graphics.width/2 - Game_Map::TILE_WIDTH/2) * 4 # X coordinate in the center of the screen + center_y = (Graphics.height/2 - Game_Map::TILE_HEIGHT/2) * 4 # Y coordinate in the center of the screen + max_x = ($game_map.width - Graphics.width*1.0/Game_Map::TILE_WIDTH) * 4 * Game_Map::TILE_WIDTH + max_y = ($game_map.height - Graphics.height*1.0/Game_Map::TILE_HEIGHT) * 4 * Game_Map::TILE_HEIGHT + count_x = ($game_map.display_x - [0,[x*Game_Map::REAL_RES_X-center_x,max_x].min].max)/Game_Map::REAL_RES_X + count_y = ($game_map.display_y - [0,[y*Game_Map::REAL_RES_Y-center_y,max_y].min].max)/Game_Map::REAL_RES_Y + if !@diag + @diag = true + dir = nil + if count_x > 0 + if count_y > 0 + dir = 7 + elsif count_y < 0 + dir = 1 + end + elsif count_x < 0 + if count_y > 0 + dir = 9 + elsif count_y < 0 + dir = 3 + end + end + count = [count_x.abs,count_y.abs].min + else + @diag = false + dir = nil + if count_x != 0 && count_y != 0 + return false + elsif count_x > 0 + dir = 4 + elsif count_x < 0 + dir = 6 + elsif count_y > 0 + dir = 8 + elsif count_y < 0 + dir = 2 + end + count = count_x != 0 ? count_x.abs : count_y.abs + end + $game_map.start_scroll(dir, count, speed) if dir != nil + if @diag + return false + else + return true + end + end + + #----------------------------------------------------------------------------- + # * Map Autoscroll (to Player) + # speed : (optional) scroll speed (from 1-6, default being 4) + #----------------------------------------------------------------------------- + def autoscroll_player(speed=SCROLL_SPEED_DEFAULT) + autoscroll($game_player.x,$game_player.y,speed) + end +end + + + +class Game_Map + def scroll_downright(distance) + @display_x = [@display_x + distance, + (self.width - Graphics.width*1.0/TILE_WIDTH) * REAL_RES_X].min + @display_y = [@display_y + distance, + (self.height - Graphics.height*1.0/TILE_HEIGHT) * REAL_RES_Y].min + end + + def scroll_downleft(distance) + @display_x = [@display_x - distance, 0].max + @display_y = [@display_y + distance, + (self.height - Graphics.height*1.0/TILE_HEIGHT) * REAL_RES_Y].min + end + + def scroll_upright(distance) + @display_x = [@display_x + distance, + (self.width - Graphics.width*1.0/TILE_WIDTH) * REAL_RES_X].min + @display_y = [@display_y - distance, 0].max + end + + def scroll_upleft(distance) + @display_x = [@display_x - distance, 0].max + @display_y = [@display_y - distance, 0].max + end + + def update_scrolling + # If scrolling + if @scroll_rest > 0 + # Change from scroll speed to distance in map coordinates + distance = (1<<@scroll_speed)*40/Graphics.frame_rate + distance = @scroll_rest if distance>@scroll_rest + # Execute scrolling + case @scroll_direction + when 1 then scroll_downleft(distance) + when 2 then scroll_down(distance) + when 3 then scroll_downright(distance) + when 4 then scroll_left(distance) + when 6 then scroll_right(distance) + when 7 then scroll_upleft(distance) + when 8 then scroll_up(distance) + when 9 then scroll_upright(distance) + end + # Subtract distance scrolled + @scroll_rest -= distance + end + end +end diff --git a/Data/Scripts/004_Game classes/006_Game_MapFactory.rb b/Data/Scripts/004_Game classes/006_Game_MapFactory.rb new file mode 100644 index 000000000..b0ba2b677 --- /dev/null +++ b/Data/Scripts/004_Game classes/006_Game_MapFactory.rb @@ -0,0 +1,526 @@ +#=============================================================================== +# Map Factory (allows multiple maps to be loaded at once and connected) +#=============================================================================== +class PokemonMapFactory + attr_reader :maps + + def initialize(id) + @maps = [] + @fixup = false + @mapChanged = false # transient instance variable + setup(id) + end + + # Clears all maps and sets up the current map with id. This function also sets + # the positions of neighboring maps and notifies the game system of a map + # change. + def setup(id) + @maps.clear + @maps[0] = Game_Map.new + @mapIndex = 0 + oldID = ($game_map) ? $game_map.map_id : 0 + setMapChanging(id,@maps[0]) if oldID!=0 && oldID!=@maps[0].map_id + $game_map = @maps[0] + @maps[0].setup(id) + setMapsInRange + setMapChanged(oldID) + end + + def map + @mapIndex = 0 if !@mapIndex || @mapIndex<0 + return @maps[@mapIndex] if @maps[@mapIndex] + raise "No maps in save file... (mapIndex=#{@mapIndex})" if @maps.length==0 + if @maps[0] + echoln("Using next map, may be incorrect (mapIndex=#{@mapIndex}, length=#{@maps.length})") + return @maps[0] + end + raise "No maps in save file... (all maps empty; mapIndex=#{@mapIndex})" + end + + def hasMap?(id) + for map in @maps + return true if map.map_id==id + end + return false + end + + def getMapIndex(id) + for i in 0...@maps.length + return i if @maps[i].map_id==id + end + return -1 + end + + def getMap(id,add=true) + for map in @maps + return map if map.map_id==id + end + map = Game_Map.new + map.setup(id) + @maps.push(map) if add + return map + end + + def getMapNoAdd(id) + return getMap(id,false) + end + + def getNewMap(playerX,playerY) + id = $game_map.map_id + conns = MapFactoryHelper.getMapConnections + if conns[id] + for conn in conns[id] + mapidB = nil + newx = 0 + newy = 0 + if conn[0] == id + mapidB = conn[3] + mapB = MapFactoryHelper.getMapDims(conn[3]) + newx = conn[4] - conn[1] + playerX + newy = conn[5] - conn[2] + playerY + else + mapidB = conn[0] + mapB = MapFactoryHelper.getMapDims(conn[0]) + newx = conn[1] - conn[4] + playerX + newy = conn[2] - conn[5] + playerY + end + if newx >= 0 && newx < mapB[0] && newy >= 0 && newy < mapB[1] + return [getMap(mapidB), newx, newy] + end + end + end + return nil + end + + # Detects whether the player has moved onto a connected map, and if so, causes + # their transfer to that map. + def setCurrentMap + return if $game_player.moving? + return if $game_map.valid?($game_player.x,$game_player.y) + newmap = getNewMap($game_player.x,$game_player.y) + return if !newmap + oldmap=$game_map.map_id + if oldmap!=0 && oldmap!=newmap[0].map_id + setMapChanging(newmap[0].map_id,newmap[0]) + end + $game_map = newmap[0] + @mapIndex = getMapIndex($game_map.map_id) + $game_player.moveto(newmap[1],newmap[2]) + $game_map.update + pbAutoplayOnTransition + $game_map.refresh + setMapChanged(oldmap) + $game_screen.weather_duration = 20 + end + + def setMapsInRange + return if @fixup + @fixup = true + id = $game_map.map_id + conns = MapFactoryHelper.getMapConnections + if conns[id] + for conn in conns[id] + if conn[0] == id + mapA = getMap(conn[0]) + newdispx = (conn[4] - conn[1]) * Game_Map::REAL_RES_X + mapA.display_x + newdispy = (conn[5] - conn[2]) * Game_Map::REAL_RES_Y + mapA.display_y + if hasMap?(conn[3]) || MapFactoryHelper.mapInRangeById?(conn[3], newdispx, newdispy) + mapB = getMap(conn[3]) + mapB.display_x = newdispx if mapB.display_x != newdispx + mapB.display_y = newdispy if mapB.display_y != newdispy + end + else + mapA = getMap(conn[3]) + newdispx = (conn[1] - conn[4]) * Game_Map::REAL_RES_X + mapA.display_x + newdispy = (conn[2] - conn[5]) * Game_Map::REAL_RES_Y + mapA.display_y + if hasMap?(conn[0]) || MapFactoryHelper.mapInRangeById?(conn[0], newdispx, newdispy) + mapB = getMap(conn[0]) + mapB.display_x = newdispx if mapB.display_x != newdispx + mapB.display_y = newdispy if mapB.display_y != newdispy + end + end + end + end + @fixup = false + end + + def setMapChanging(newID,newMap) + Events.onMapChanging.trigger(self,newID,newMap) + end + + def setMapChanged(prevMap) + Events.onMapChange.trigger(self,prevMap) + @mapChanged = true + end + + def setSceneStarted(scene) + Events.onMapSceneChange.trigger(self,scene,@mapChanged) + @mapChanged = false + end + + # Similar to Game_Player#passable?, but supports map connections + def isPassableFromEdge?(x, y) + return true if $game_map.valid?(x, y) + newmap = getNewMap(x, y) + return false if !newmap + return isPassable?(newmap[0].map_id, newmap[1], newmap[2]) + end + + def isPassable?(mapID, x, y, thisEvent = nil) + thisEvent = $game_player if !thisEvent + map = getMapNoAdd(mapID) + return false if !map + return false if !map.valid?(x, y) + return true if thisEvent.through + # Check passability of tile + if thisEvent.is_a?(Game_Player) + return false unless ($DEBUG && Input.press?(Input::CTRL)) || + map.passable?(x, y, 0, thisEvent) + else + return false unless map.passable?(x, y, 0, thisEvent) + end + # Check passability of event(s) in that spot + for event in map.events.values + next if event == thisEvent || !event.at_coordinate?(x, y) + return false if !event.through && event.character_name != "" + end + # Check passability of player + if !thisEvent.is_a?(Game_Player) + if $game_map.map_id == mapID && $game_player.x == x && $game_player.y == y + return false if !$game_player.through && $game_player.character_name != "" + end + end + return true + end + + # Only used by dependent events + def isPassableStrict?(mapID,x,y,thisEvent=nil) + thisEvent = $game_player if !thisEvent + map = getMapNoAdd(mapID) + return false if !map + return false if !map.valid?(x,y) + return true if thisEvent.through + if thisEvent==$game_player + if !($DEBUG && Input.press?(Input::CTRL)) + return false if !map.passableStrict?(x,y,0,thisEvent) + end + else + return false if !map.passableStrict?(x,y,0,thisEvent) + end + for event in map.events.values + next if event == thisEvent || !event.at_coordinate?(x, y) + return false if !event.through && event.character_name!="" + end + return true + end + + def getTerrainTag(mapid,x,y,countBridge=false) + map = getMapNoAdd(mapid) + return map.terrain_tag(x,y,countBridge) + end + + # NOTE: Assumes the event is 1x1 tile in size. Only returns one terrain tag. + def getFacingTerrainTag(dir=nil,event=nil) + tile = getFacingTile(dir,event) + return GameData::TerrainTag.get(:None) if !tile + return getTerrainTag(tile[0],tile[1],tile[2]) + end + + def getTerrainTagFromCoords(mapid,x,y,countBridge=false) + tile = getRealTilePos(mapid,x,y) + return GameData::TerrainTag.get(:None) if !tile + return getTerrainTag(tile[0],tile[1],tile[2]) + end + + def areConnected?(mapID1, mapID2) + return true if mapID1 == mapID2 + conns = MapFactoryHelper.getMapConnections + if conns[mapID1] + for conn in conns[mapID1] + return true if conn[0] == mapID2 || conn[3] == mapID2 + end + end + return false + end + + def getRelativePos(thisMapID, thisX, thisY, otherMapID, otherX, otherY) + if thisMapID == otherMapID # Both events share the same map + return [otherX - thisX, otherY - thisY] + end + conns = MapFactoryHelper.getMapConnections + if conns[thisMapID] + for conn in conns[thisMapID] + if conn[0] == otherMapID + posX = thisX + conn[1] - conn[4] + otherX + posY = thisY + conn[2] - conn[5] + otherY + return [posX, posY] + elsif conn[3] == otherMapID + posX = thisX + conn[4] - conn[1] + otherX + posY = thisY + conn[5] - conn[2] + otherY + return [posX, posY] + end + end + end + return [0, 0] + end + + # Gets the distance from this event to another event. Example: If this event's + # coordinates are (2,5) and the other event's coordinates are (5,1), returns + # the array (3,-4), because (5-2=3) and (1-5=-4). + def getThisAndOtherEventRelativePos(thisEvent,otherEvent) + return [0,0] if !thisEvent || !otherEvent + return getRelativePos( + thisEvent.map.map_id,thisEvent.x,thisEvent.y, + otherEvent.map.map_id,otherEvent.x,otherEvent.y) + end + + def getThisAndOtherPosRelativePos(thisEvent,otherMapID,otherX,otherY) + return [0,0] if !thisEvent + return getRelativePos( + thisEvent.map.map_id,thisEvent.x,thisEvent.y,otherMapID,otherX,otherY) + end + + # Unused + def getOffsetEventPos(event,xOffset,yOffset) + event = $game_player if !event + return nil if !event + return getRealTilePos(event.map.map_id,event.x+xOffset,event.y+yOffset) + end + + # NOTE: Assumes the event is 1x1 tile in size. Only returns one tile. + def getFacingTile(direction=nil,event=nil,steps=1) + event = $game_player if event==nil + return [0,0,0] if !event + x = event.x + y = event.y + id = event.map.map_id + direction = event.direction if direction==nil + return getFacingTileFromPos(id,x,y,direction,steps) + end + + def getFacingTileFromPos(mapID,x,y,direction=0,steps=1) + id = mapID + case direction + when 1 + x -= steps + y += steps + when 2 + y += steps + when 3 + x += steps + y += steps + when 4 + x -= steps + when 6 + x += steps + when 7 + x -= steps + y -= steps + when 8 + y -= steps + when 9 + x += steps + y -= steps + else + return [id,x,y] + end + return getRealTilePos(mapID,x,y) + end + + def getRealTilePos(mapID, x, y) + id = mapID + return [id, x, y] if getMapNoAdd(id).valid?(x, y) + conns = MapFactoryHelper.getMapConnections + if conns[id] + for conn in conns[id] + if conn[0] == id + newX = x + conn[4] - conn[1] + newY = y + conn[5] - conn[2] + next if newX < 0 || newY < 0 + dims = MapFactoryHelper.getMapDims(conn[3]) + next if newX >= dims[0] || newY >= dims[1] + return [conn[3], newX, newY] + else + newX = x + conn[1] - conn[4] + newY = y + conn[2] - conn[5] + next if newX < 0 || newY < 0 + dims = MapFactoryHelper.getMapDims(conn[0]) + next if newX >= dims[0] || newY >= dims[1] + return [conn[0], newX, newY] + end + end + end + return nil + end + + def getFacingCoords(x,y,direction=0,steps=1) + case direction + when 1 + x -= steps + y += steps + when 2 + y += steps + when 3 + x += steps + y += steps + when 4 + x -= steps + when 6 + x += steps + when 7 + x -= steps + y -= steps + when 8 + y -= steps + when 9 + x += steps + y -= steps + end + return [x,y] + end + + def updateMaps(scene) + updateMapsInternal + $MapFactory.setSceneStarted(scene) if @mapChanged + end + + def updateMapsInternal + return if $game_player.moving? + if !MapFactoryHelper.hasConnections?($game_map.map_id) + return if @maps.length==1 + for i in 0...@maps.length + @maps[i] = nil if $game_map.map_id!=@maps[i].map_id + end + @maps.compact! + @mapIndex = getMapIndex($game_map.map_id) + return + end + setMapsInRange + deleted = false + for i in 0...@maps.length + next if MapFactoryHelper.mapInRange?(@maps[i]) + @maps[i] = nil + deleted = true + end + if deleted + @maps.compact! + @mapIndex = getMapIndex($game_map.map_id) + end + end +end + +#=============================================================================== +# Map Factory Helper (stores map connection and size data and calculations +# involving them) +#=============================================================================== +module MapFactoryHelper + @@MapConnections = nil + @@MapDims = nil + + def self.clear + @@MapConnections = nil + @@MapDims = nil + end + + def self.getMapConnections + if !@@MapConnections + @@MapConnections = [] + conns = load_data("Data/map_connections.dat") + conns.each do |conn| + # Ensure both maps in a connection are valid + dimensions = getMapDims(conn[0]) + next if dimensions[0] == 0 || dimensions[1] == 0 + dimensions = getMapDims(conn[3]) + next if dimensions[0] == 0 || dimensions[1] == 0 + # Convert first map's edge and coordinate to pair of coordinates + edge = getMapEdge(conn[0], conn[1]) + case conn[1] + when "N", "S" + conn[1] = conn[2] + conn[2] = edge + when "E", "W" + conn[1] = edge + end + # Convert second map's edge and coordinate to pair of coordinates + edge = getMapEdge(conn[3], conn[4]) + case conn[4] + when "N", "S" + conn[4] = conn[5] + conn[5] = edge + when "E", "W" + conn[4] = edge + end + # Add connection to arrays for both maps + @@MapConnections[conn[0]] = [] if !@@MapConnections[conn[0]] + @@MapConnections[conn[0]].push(conn) + @@MapConnections[conn[3]] = [] if !@@MapConnections[conn[3]] + @@MapConnections[conn[3]].push(conn) + end + end + return @@MapConnections + end + + def self.hasConnections?(id) + conns = MapFactoryHelper.getMapConnections + return conns[id] ? true : false + end + + # Gets the height and width of the map with id + def self.getMapDims(id) + # Create cache if doesn't exist + @@MapDims = [] if !@@MapDims + # Add map to cache if can't be found + if !@@MapDims[id] + begin + map = load_data(sprintf("Data/Map%03d.rxdata", id)) + @@MapDims[id] = [map.width,map.height] + rescue + @@MapDims[id] = [0,0] + end + end + # Return map in cache + return @@MapDims[id] + end + + # Returns the X or Y coordinate of an edge on the map with id. + # Considers the special strings "N","W","E","S" + def self.getMapEdge(id,edge) + return 0 if edge=="N" || edge=="W" + dims = getMapDims(id) # Get dimensions + return dims[0] if edge=="E" + return dims[1] if edge=="S" + return dims[0] # real dimension (use width) + end + + def self.mapInRange?(map) + range = 6 # Number of tiles + dispx = map.display_x + dispy = map.display_y + return false if dispx >= (map.width + range) * Game_Map::REAL_RES_X + return false if dispy >= (map.height + range) * Game_Map::REAL_RES_Y + return false if dispx <= -(Graphics.width + range * Game_Map::TILE_WIDTH) * Game_Map::X_SUBPIXELS + return false if dispy <= -(Graphics.height + range * Game_Map::TILE_HEIGHT) * Game_Map::Y_SUBPIXELS + return true + end + + def self.mapInRangeById?(id,dispx,dispy) + range = 6 # Number of tiles + dims = MapFactoryHelper.getMapDims(id) + return false if dispx >= (dims[0] + range) * Game_Map::REAL_RES_X + return false if dispy >= (dims[1] + range) * Game_Map::REAL_RES_Y + return false if dispx <= -(Graphics.width + range * Game_Map::TILE_WIDTH) * Game_Map::X_SUBPIXELS + return false if dispy <= -(Graphics.height + range * Game_Map::TILE_HEIGHT) * Game_Map::Y_SUBPIXELS + return true + end +end + +#=============================================================================== +# +#=============================================================================== +# Unused +def updateTilesets + maps = $MapFactory.maps + for map in maps + map.updateTileset if map + end +end diff --git a/Data/Scripts/004_Game classes/007_Game_Character.rb b/Data/Scripts/004_Game classes/007_Game_Character.rb new file mode 100644 index 000000000..fd7aa6495 --- /dev/null +++ b/Data/Scripts/004_Game classes/007_Game_Character.rb @@ -0,0 +1,1178 @@ +class Game_Character + attr_reader :id + attr_reader :original_x + attr_reader :original_y + attr_reader :x + attr_reader :y + attr_reader :real_x + attr_reader :real_y + attr_accessor :width + attr_accessor :height + attr_accessor :sprite_size + attr_reader :tile_id + attr_accessor :character_name + attr_accessor :character_hue + attr_reader :opacity + attr_reader :blend_type + attr_accessor :direction + attr_accessor :pattern + attr_accessor :pattern_surf + attr_accessor :lock_pattern + attr_reader :move_route_forcing + attr_accessor :through + attr_accessor :animation_id + attr_accessor :transparent + attr_reader :move_speed + attr_accessor :walk_anime + attr_writer :bob_height + attr_accessor :under_everything + + def initialize(map = nil) + @map = map + @id = 0 + @original_x = 0 + @original_y = 0 + @x = 0 + @y = 0 + @real_x = 0 + @real_y = 0 + @width = 1 + @height = 1 + @sprite_size = [Game_Map::TILE_WIDTH, Game_Map::TILE_HEIGHT] + @tile_id = 0 + @character_name = "" + @character_hue = 0 + @opacity = 255 + @blend_type = 0 + @direction = 2 + @pattern = 0 + @pattern_surf = 0 + @lock_pattern = false + @move_route_forcing = false + @through = false + @animation_id = 0 + @transparent = false + @original_direction = 2 + @original_pattern = 0 + @move_type = 0 + self.move_speed = 3 + self.move_frequency = 6 + @move_route = nil + @move_route_index = 0 + @original_move_route = nil + @original_move_route_index = 0 + @walk_anime = true # Whether character should animate while moving + @step_anime = false # Whether character should animate while still + @direction_fix = false + @always_on_top = false + @anime_count = 0 + @stop_count = 0 + @jump_peak = 0 # Max height while jumping + @jump_distance = 0 # Total distance of jump + @jump_distance_left = 0 # Distance left to travel + @jump_count = 0 # Frames left in a stationary jump + @bob_height = 0 + @wait_count = 0 + @moved_this_frame = false + @locked = false + @prelock_direction = 0 + @under_everything=false + @forced_z=nil + end + + def at_coordinate?(check_x, check_y) + return check_x >= @x && check_x < @x + @width && + check_y > @y - @height && check_y <= @y + end + + def in_line_with_coordinate?(check_x, check_y) + return (check_x >= @x && check_x < @x + @width) || + (check_y > @y - @height && check_y <= @y) + end + + def each_occupied_tile + for i in @x...(@x + @width) + for j in (@y - @height + 1)..@y + yield i, j + end + end + end + + def set_opacity(opacity) + @opacity = opacity + end + + def move_speed=(val) + return if val == @move_speed + @move_speed = val + # @move_speed_real is the number of quarter-pixels to move each frame. There + # are 128 quarter-pixels per tile. By default, it is calculated from + # @move_speed and has these values (assuming 40 fps): + # 1 => 3.2 # 40 frames per tile + # 2 => 6.4 # 20 frames per tile + # 3 => 12.8 # 10 frames per tile - walking speed + # 4 => 25.6 # 5 frames per tile - running speed (2x walking speed) + # 5 => 32 # 4 frames per tile - cycling speed (1.25x running speed) + # 6 => 64 # 2 frames per tile + self.move_speed_real = (val == 6) ? 64 : (val == 5) ? 32 : (2 ** (val + 1)) * 0.8 + end + + def move_speed_real + self.move_speed = @move_speed if !@move_speed_real + return @move_speed_real + end + + def move_speed_real=(val) + @move_speed_real = val * 40.0 / Graphics.frame_rate + end + + def jump_speed_real + self.jump_speed_real = (2 ** (3 + 1)) * 0.8 if !@jump_speed_real # 3 is walking speed + return @jump_speed_real + end + + def jump_speed_real=(val) + @jump_speed_real = val * 40.0 / Graphics.frame_rate + end + + def move_frequency=(val) + return if val == @move_frequency + @move_frequency = val + # @move_frequency_real is the number of frames to wait between each action + # in a move route (not forced). Specifically, this is the number of frames + # to wait after the character stops moving because of the previous action. + # By default, it is calculated from @move_frequency and has these values + # (assuming 40 fps): + # 1 => 190 # 4.75 seconds + # 2 => 144 # 3.6 seconds + # 3 => 102 # 2.55 seconds + # 4 => 64 # 1.6 seconds + # 5 => 30 # 0.75 seconds + # 6 => 0 # 0 seconds, i.e. continuous movement + self.move_frequency_real = (40 - val * 2) * (6 - val) + end + + def move_frequency_real + self.move_frequency = @move_frequency if !@move_frequency_real + return @move_frequency_real + end + + def move_frequency_real=(val) + @move_frequency_real = val * Graphics.frame_rate / 40.0 + end + + def bob_height + @bob_height = 0 if !@bob_height + return @bob_height + end + + def lock + return if @locked + @prelock_direction = 0 # Was @direction but disabled + turn_toward_player + @locked = true + end + + def minilock + @prelock_direction = 0 # Was @direction but disabled + @locked = true + end + + def lock? + return @locked + end + + def unlock + return unless @locked + @locked = false + @direction = @prelock_direction if !@direction_fix && @prelock_direction != 0 + end + + #============================================================================= + # Information from map data + #============================================================================= + def map + return (@map) ? @map : $game_map + end + + def terrain_tag + return self.map.terrain_tag(@x, @y) + end + + def cancelMoveRoute() + @move_route = nil + @move_route_forcing = false + end + + def bush_depth + return @bush_depth || 0 + end + + def calculate_bush_depth + if @tile_id > 0 || @always_on_top || jumping? + @bush_depth = 0 + else + deep_bush = regular_bush = false + xbehind = @x + (@direction == 4 ? 1 : @direction == 6 ? -1 : 0) + ybehind = @y + (@direction == 8 ? 1 : @direction == 2 ? -1 : 0) + this_map = (self.map.valid?(@x, @y)) ? [self.map, @x, @y] : $MapFactory.getNewMap(@x, @y) + if this_map[0].deepBush?(this_map[1], this_map[2]) && self.map.deepBush?(xbehind, ybehind) + @bush_depth = Game_Map::TILE_HEIGHT + elsif !moving? && this_map[0].bush?(this_map[1], this_map[2]) + @bush_depth = 12 + else + @bush_depth = 0 + end + end + end + + #============================================================================= + # Passability + #============================================================================= + def pbFacingTerrainTag(dir = nil) + dir = self.direction if !dir + return $MapFactory.getFacingTerrainTag(dir, self) if $MapFactory + facing = pbFacingTile(dir, self) + return $game_map.terrain_tag(facing[1], facing[2]) + end + + + def passable?(x, y, d, strict = false) + return false if self == $game_player && $game_switches[SWITCH_LOCK_PLAYER_MOVEMENT] + new_x = x + (d == 6 ? 1 : d == 4 ? -1 : 0) + new_y = y + (d == 2 ? 1 : d == 8 ? -1 : 0) + return false unless self.map.valid?(new_x, new_y) + if self.character_name == "SHARPEDO" || self.character_name == "nightmare" + return false if pbFacingTerrainTag().id==:SharpedoObstacle + end + return true if @through + + if strict + return false unless self.map.passableStrict?(x, y, d, self) + return false unless self.map.passableStrict?(new_x, new_y, 10 - d, self) + else + return false unless self.map.passable?(x, y, d, self) + return false unless self.map.passable?(new_x, new_y, 10 - d, self) + end + for event in self.map.events.values + next if self == event || !event.at_coordinate?(new_x, new_y) || event.through + return false if self != $game_player || event.character_name != "" + end + if $game_player.x == new_x && $game_player.y == new_y + return false if !$game_player.through && @character_name != "" + end + return true + end + + def can_move_from_coordinate?(start_x, start_y, dir, strict = false) + case dir + when 2, 8 # Down, up + y_diff = (dir == 8) ? @height - 1 : 0 + for i in start_x...(start_x + @width) + return false if !passable?(i, start_y - y_diff, dir, strict) + end + return true + when 4, 6 # Left, right + x_diff = (dir == 6) ? @width - 1 : 0 + for i in (start_y - @height + 1)..start_y + return false if !passable?(start_x + x_diff, i, dir, strict) + end + return true + when 1, 3 # Down diagonals + # Treated as moving down first and then horizontally, because that + # describes which tiles the character's feet touch + for i in start_x...(start_x + @width) + return false if !passable?(i, start_y, 2, strict) + end + x_diff = (dir == 3) ? @width - 1 : 0 + for i in (start_y - @height + 1)..start_y + return false if !passable?(start_x + x_diff, i + 1, dir + 3, strict) + end + return true + when 7, 9 # Up diagonals + # Treated as moving horizontally first and then up, because that describes + # which tiles the character's feet touch + x_diff = (dir == 9) ? @width - 1 : 0 + for i in (start_y - @height + 1)..start_y + return false if !passable?(start_x + x_diff, i, dir - 3, strict) + end + x_offset = (dir == 9) ? 1 : -1 + for i in start_x...(start_x + @width) + return false if !passable?(i + x_offset, start_y - @height + 1, 8, strict) + end + return true + end + return false + end + + def can_move_in_direction?(dir, strict = false) + return can_move_from_coordinate?(@x, @y, dir, strict) + end + + #============================================================================= + # Screen position of the character + #============================================================================= + def screen_x + ret = ((@real_x.to_f - self.map.display_x) / Game_Map::X_SUBPIXELS).round + ret += @width * Game_Map::TILE_WIDTH / 2 + return ret + end + + def screen_y_ground + ret = ((@real_y.to_f - self.map.display_y) / Game_Map::Y_SUBPIXELS).round + ret += Game_Map::TILE_HEIGHT + return ret + end + + def screen_y + ret = screen_y_ground + if jumping? + if @jump_count > 0 + jump_fraction = ((@jump_count * jump_speed_real / Game_Map::REAL_RES_X) - 0.5).abs # 0.5 to 0 to 0.5 + else + jump_fraction = ((@jump_distance_left / @jump_distance) - 0.5).abs # 0.5 to 0 to 0.5 + end + ret += @jump_peak * (4 * jump_fraction ** 2 - 1) + end + return ret + end + + def screen_z(height = 0) + return -1 if @under_everything + return 999 if @always_on_top + return @forced_z if @forced_z + z = screen_y_ground + if @tile_id > 0 + begin + return z + self.map.priorities[@tile_id] * 32 + rescue + return 0 + #raise "Event's graphic is an out-of-range tile (event #{@id}, map #{self.map.map_id})" + end + end + # Add z if height exceeds 32 + return z + ((height > Game_Map::TILE_HEIGHT) ? Game_Map::TILE_HEIGHT - 1 : 0) + end + + #============================================================================= + # Movement + #============================================================================= + def moving? + return @real_x != @x * Game_Map::REAL_RES_X || + @real_y != @y * Game_Map::REAL_RES_Y + end + + def jumping? + return (@jump_distance_left || 0) > 0 || @jump_count > 0 + end + + def straighten + @pattern = 0 if @walk_anime || @step_anime + @anime_count = 0 + @prelock_direction = 0 + end + + def force_move_route(move_route) + #echoln screen_z() if self == $game_player + if @original_move_route == nil + @original_move_route = @move_route + @original_move_route_index = @move_route_index + end + @move_route = move_route + @move_route_index = 0 + @move_route_forcing = true + @prelock_direction = 0 + @wait_count = 0 + move_type_custom + end + + def moveto(x, y) + @x = x % self.map.width + @y = y % self.map.height + @real_x = @x * Game_Map::REAL_RES_X + @real_y = @y * Game_Map::REAL_RES_Y + @prelock_direction = 0 + calculate_bush_depth + triggerLeaveTile + end + + def triggerLeaveTile + if @oldX && @oldY && @oldMap && + (@oldX != self.x || @oldY != self.y || @oldMap != self.map.map_id) + Events.onLeaveTile.trigger(self, self, @oldMap, @oldX, @oldY) + end + @oldX = self.x + @oldY = self.y + @oldMap = self.map.map_id + end + + def increase_steps + @stop_count = 0 + triggerLeaveTile + end + + #============================================================================= + # Movement commands + #============================================================================= + def move_type_random + case rand(6) + when 0..3 then + move_random + when 4 then + move_forward + when 5 then + @stop_count = 0 + end + end + + def move_type_toward_player + sx = @x + @width / 2.0 - ($game_player.x + $game_player.width / 2.0) + sy = @y - @height / 2.0 - ($game_player.y - $game_player.height / 2.0) + if sx.abs + sy.abs >= 20 + move_random + return + end + case rand(6) + when 0..3 then + move_toward_player + when 4 then + move_random + when 5 then + move_forward + end + end + + def move_type_custom + return if jumping? || moving? + while @move_route_index < @move_route.list.size + command = @move_route.list[@move_route_index] + if command.code == 0 + if @move_route.repeat + @move_route_index = 0 + else + if @move_route_forcing + @move_route_forcing = false + @move_route = @original_move_route + @move_route_index = @original_move_route_index + @original_move_route = nil + end + @stop_count = 0 + end + return + end + if command.code <= 14 + case command.code + when 1 then + move_down + when 2 then + move_left + when 3 then + move_right + when 4 then + move_up + when 5 then + move_lower_left + when 6 then + move_lower_right + when 7 then + move_upper_left + when 8 then + move_upper_right + when 9 then + move_random + when 10 then + move_toward_player + when 11 then + move_away_from_player + when 12 then + move_forward + when 13 then + move_backward + when 14 then + jump(command.parameters[0], command.parameters[1]) + end + @move_route_index += 1 if @move_route.skippable || moving? || jumping? + return + end + if command.code == 15 # Wait + @wait_count = (command.parameters[0] * Graphics.frame_rate / 20) - 1 + @move_route_index += 1 + return + end + if command.code >= 16 && command.code <= 26 + case command.code + when 16 then + turn_down + when 17 then + turn_left + when 18 then + turn_right + when 19 then + turn_up + when 20 then + turn_right_90 + when 21 then + turn_left_90 + when 22 then + turn_180 + when 23 then + turn_right_or_left_90 + when 24 then + turn_random + when 25 then + turn_toward_player + when 26 then + turn_away_from_player + end + @move_route_index += 1 + return + end + if command.code >= 27 + case command.code + when 27 + $game_switches[command.parameters[0]] = true + self.map.need_refresh = true + when 28 + $game_switches[command.parameters[0]] = false + self.map.need_refresh = true + when 29 then + self.move_speed = command.parameters[0] + when 30 then + self.move_frequency = command.parameters[0] + when 31 then + @walk_anime = true + when 32 then + @walk_anime = false + when 33 then + @step_anime = true + when 34 then + @step_anime = false + when 35 then + @direction_fix = true + when 36 then + @direction_fix = false + when 37 then + @through = true + when 38 then + @through = false + when 39 + old_always_on_top = @always_on_top + @always_on_top = true + calculate_bush_depth if @always_on_top != old_always_on_top + when 40 + old_always_on_top = @always_on_top + @always_on_top = false + calculate_bush_depth if @always_on_top != old_always_on_top + when 41 + old_tile_id = @tile_id + @tile_id = 0 + @character_name = command.parameters[0] + @character_hue = command.parameters[1] + if @original_direction != command.parameters[2] + @direction = command.parameters[2] + @original_direction = @direction + @prelock_direction = 0 + end + if @original_pattern != command.parameters[3] + @pattern = command.parameters[3] + @original_pattern = @pattern + end + calculate_bush_depth if @tile_id != old_tile_id + when 42 then + @opacity = command.parameters[0] + when 43 then + @blend_type = command.parameters[0] + when 44 then + pbSEPlay(command.parameters[0]) + when 45 then + eval(command.parameters[0]) + end + @move_route_index += 1 + end + end + end + + def move_generic(dir, turn_enabled = true) + turn_generic(dir) if turn_enabled + if can_move_in_direction?(dir) + turn_generic(dir) + @x += (dir == 4) ? -1 : (dir == 6) ? 1 : 0 + @y += (dir == 8) ? -1 : (dir == 2) ? 1 : 0 + increase_steps + else + check_event_trigger_touch(dir) + end + end + + def move_down(turn_enabled = true) + move_generic(2, turn_enabled) + end + + def move_left(turn_enabled = true) + move_generic(4, turn_enabled) + end + + def move_right(turn_enabled = true) + move_generic(6, turn_enabled) + end + + def move_up(turn_enabled = true) + move_generic(8, turn_enabled) + end + + # def move_upper_left + # @through=true + # unless @direction_fix + # @direction = (@direction == 6 ? 4 : @direction == 2 ? 8 : @direction) + # end + # if can_move_in_direction?(7) + # @x -= 1 + # @y -= 1 + # increase_steps + # end + # @through=false + # end + # + # def move_upper_right + # @through=true + # unless @direction_fix + # @direction = (@direction == 4 ? 6 : @direction == 2 ? 8 : @direction) + # end + # if can_move_in_direction?(9) + # @x += 1 + # @y -= 1 + # increase_steps + # end + # @through=false + # end + # + # def move_lower_left + # @through=true + # unless @direction_fix + # @direction = (@direction == 6 ? 4 : @direction == 8 ? 2 : @direction) + # end + # if can_move_in_direction?(1) + # @x -= 1 + # @y += 1 + # increase_steps + # end + # @through=false + # end + # + # def move_lower_right + # @through=true + # unless @direction_fix + # @direction = (@direction == 4 ? 6 : @direction == 8 ? 2 : @direction) + # end + # if can_move_in_direction?(3) + # @x += 1 + # @y += 1 + # increase_steps + # end + # @through=false + # end + + def move_lower_left + unless @direction_fix + @direction = ( + if @direction == DIRECTION_RIGHT + DIRECTION_LEFT + else + @direction == DIRECTION_UP ? DIRECTION_DOWN : @direction + end) + end + if (passable?(@x, @y, DIRECTION_DOWN) and passable?(@x, @y + 1, DIRECTION_LEFT)) or + (passable?(@x, @y, DIRECTION_LEFT) and passable?(@x - 1, @y, DIRECTION_DOWN)) + if destination_is_passable(@x - 1, @y + 1) + @x -= 1 + @y += 1 + increase_steps + end + end + end + + def move_lower_right + unless @direction_fix + @direction = ( + if @direction == DIRECTION_LEFT + DIRECTION_RIGHT + else + @direction == DIRECTION_UP ? DIRECTION_DOWN : @direction + end) + end + if (passable?(@x, @y, DIRECTION_DOWN) and passable?(@x, @y + 1, DIRECTION_RIGHT)) or + (passable?(@x, @y, DIRECTION_RIGHT) and passable?(@x + 1, @y, DIRECTION_DOWN)) + if destination_is_passable(@x + 1, @y + 1) + @x += 1 + @y += 1 + increase_steps + end + end + end + + def move_upper_left + unless @direction_fix + @direction = ( + if @direction == DIRECTION_RIGHT + DIRECTION_LEFT + else + @direction == DIRECTION_DOWN ? DIRECTION_UP : @direction + end) + end + if (passable?(@x, @y, DIRECTION_UP) and passable?(@x, @y - 1, DIRECTION_LEFT)) or + (passable?(@x, @y, DIRECTION_LEFT) and passable?(@x - 1, @y, DIRECTION_UP)) + if destination_is_passable(@x - 1, @y - 1) + @x -= 1 + @y -= 1 + increase_steps + end + end + end + + def move_upper_right + unless @direction_fix + @direction = ( + if @direction == DIRECTION_LEFT + DIRECTION_RIGHT + else + @direction == DIRECTION_DOWN ? DIRECTION_UP : @direction + end) + end + if (passable?(@x, @y, DIRECTION_UP) and passable?(@x, @y - 1, DIRECTION_RIGHT)) or + (passable?(@x, @y, DIRECTION_RIGHT) and passable?(@x + 1, @y, DIRECTION_UP)) + if destination_is_passable(@x + 1, @y - 1) + @x += 1 + @y -= 1 + increase_steps + end + end + end + + def destination_is_passable(x_dest, y_dest) + return passable?(x_dest, y_dest, 0) + end + + def moveLeft90 # anticlockwise + case self.direction + when 2 then + move_right # down + when 4 then + move_down # left + when 6 then + move_up # right + when 8 then + move_left # up + end + end + + def moveRight90 # clockwise + case self.direction + when 2 then + move_left # down + when 4 then + move_up # left + when 6 then + move_down # right + when 8 then + move_right # up + end + end + + def move_random + case rand(4) + when 0 then + move_down(false) + when 1 then + move_left(false) + when 2 then + move_right(false) + when 3 then + move_up(false) + end + end + + def move_random_range(xrange = -1, yrange = -1) + dirs = [] # 0=down, 1=left, 2=right, 3=up + if xrange < 0 + dirs.push(1); dirs.push(2) + elsif xrange > 0 + dirs.push(1) if @x > @original_x - xrange + dirs.push(2) if @x < @original_x + xrange + end + if yrange < 0 + dirs.push(0); dirs.push(3) + elsif yrange > 0 + dirs.push(0) if @y < @original_y + yrange + dirs.push(3) if @y > @original_y - yrange + end + return if dirs.length == 0 + case dirs[rand(dirs.length)] + when 0 then + move_down(false) + when 1 then + move_left(false) + when 2 then + move_right(false) + when 3 then + move_up(false) + end + end + + def move_random_UD(range = -1) + move_random_range(0, range) + end + + def move_random_LR(range = -1) + move_random_range(range, 0) + end + + def move_toward_player + sx = @x + @width / 2.0 - ($game_player.x + $game_player.width / 2.0) + sy = @y - @height / 2.0 - ($game_player.y - $game_player.height / 2.0) + return if sx == 0 && sy == 0 + abs_sx = sx.abs + abs_sy = sy.abs + if abs_sx == abs_sy + (rand(2) == 0) ? abs_sx += 1 : abs_sy += 1 + end + if abs_sx > abs_sy + (sx > 0) ? move_left : move_right + if !moving? && sy != 0 + (sy > 0) ? move_up : move_down + end + else + (sy > 0) ? move_up : move_down + if !moving? && sx != 0 + (sx > 0) ? move_left : move_right + end + end + end + + def move_away_from_player + sx = @x + @width / 2.0 - ($game_player.x + $game_player.width / 2.0) + sy = @y - @height / 2.0 - ($game_player.y - $game_player.height / 2.0) + return if sx == 0 && sy == 0 + abs_sx = sx.abs + abs_sy = sy.abs + if abs_sx == abs_sy + (rand(2) == 0) ? abs_sx += 1 : abs_sy += 1 + end + if abs_sx > abs_sy + (sx > 0) ? move_right : move_left + if !moving? && sy != 0 + (sy > 0) ? move_down : move_up + end + else + (sy > 0) ? move_down : move_up + if !moving? && sx != 0 + (sx > 0) ? move_right : move_left + end + end + end + + def move_forward + case @direction + when 2 then + move_down(false) + when 4 then + move_left(false) + when 6 then + move_right(false) + when 8 then + move_up(false) + end + end + + def move_backward + last_direction_fix = @direction_fix + @direction_fix = true + case @direction + when 2 then + move_up(false) + when 4 then + move_right(false) + when 6 then + move_left(false) + when 8 then + move_down(false) + end + @direction_fix = last_direction_fix + end + + def jump(x_plus, y_plus) + if x_plus != 0 || y_plus != 0 + if x_plus.abs > y_plus.abs + (x_plus < 0) ? turn_left : turn_right + else + (y_plus < 0) ? turn_up : turn_down + end + each_occupied_tile { |i, j| return if !passable?(i + x_plus, j + y_plus, 0) } + end + @x = @x + x_plus + @y = @y + y_plus + real_distance = Math::sqrt(x_plus * x_plus + y_plus * y_plus) + distance = [1, real_distance].max + @jump_peak = distance * Game_Map::TILE_HEIGHT * 3 / 8 # 3/4 of tile for ledge jumping + @jump_distance = [x_plus.abs * Game_Map::REAL_RES_X, y_plus.abs * Game_Map::REAL_RES_Y].max + @jump_distance_left = 1 # Just needs to be non-zero + if real_distance > 0 # Jumping to somewhere else + @jump_count = 0 + else + # Jumping on the spot + @jump_speed_real = nil # Reset jump speed + @jump_count = Game_Map::REAL_RES_X / jump_speed_real # Number of frames to jump one tile + end + @stop_count = 0 + if self.is_a?(Game_Player) + $PokemonTemp.dependentEvents.pbMoveDependentEvents + end + triggerLeaveTile + end + + def jumpForward + case self.direction + when 2 then + jump(0, 1) # down + when 4 then + jump(-1, 0) # left + when 6 then + jump(1, 0) # right + when 8 then + jump(0, -1) # up + end + end + + def jumpBackward + case self.direction + when 2 then + jump(0, -1) # down + when 4 then + jump(1, 0) # left + when 6 then + jump(-1, 0) # right + when 8 then + jump(0, 1) # up + end + end + + def turn_generic(dir) + return if @direction_fix + oldDirection = @direction + @direction = dir + @stop_count = 0 + pbCheckEventTriggerAfterTurning if dir != oldDirection + end + + def turn_down + turn_generic(2); + end + + def turn_left + turn_generic(4); + end + + def turn_right + turn_generic(6); + end + + def turn_up + turn_generic(8); + end + + def turn_right_90 + case @direction + when 2 then + turn_left + when 4 then + turn_up + when 6 then + turn_down + when 8 then + turn_right + end + end + + def turn_left_90 + case @direction + when 2 then + turn_right + when 4 then + turn_down + when 6 then + turn_up + when 8 then + turn_left + end + end + + def turn_180 + case @direction + when 2 then + turn_up + when 4 then + turn_right + when 6 then + turn_left + when 8 then + turn_down + end + end + + def turn_right_or_left_90 + (rand(2) == 0) ? turn_right_90 : turn_left_90 + end + + def turn_random + case rand(4) + when 0 then + turn_up + when 1 then + turn_right + when 2 then + turn_left + when 3 then + turn_down + end + end + + def turn_toward_player + sx = @x + @width / 2.0 - ($game_player.x + $game_player.width / 2.0) + sy = @y - @height / 2.0 - ($game_player.y - $game_player.height / 2.0) + return if sx == 0 && sy == 0 + if sx.abs > sy.abs + (sx > 0) ? turn_left : turn_right + else + (sy > 0) ? turn_up : turn_down + end + end + + def turn_away_from_player + sx = @x + @width / 2.0 - ($game_player.x + $game_player.width / 2.0) + sy = @y - @height / 2.0 - ($game_player.y - $game_player.height / 2.0) + return if sx == 0 && sy == 0 + if sx.abs > sy.abs + (sx > 0) ? turn_right : turn_left + else + (sy > 0) ? turn_down : turn_up + end + end + + #============================================================================= + # Updating + #============================================================================= + def update + @moved_last_frame = @moved_this_frame + @stopped_last_frame = @stopped_this_frame + if !$game_temp.in_menu + # Update command + update_command + # Update movement + (moving? || jumping?) ? update_move : update_stop + end + # Update animation + update_pattern + end + + def update_command + if @wait_count > 0 + @wait_count -= 1 + elsif @move_route_forcing + move_type_custom + elsif !@starting && !lock? && !moving? && !jumping? + update_command_new + end + end + + def update_command_new + # @stop_count is the number of frames since the last movement finished. + # @move_frequency has these values: + # 1 => @stop_count > 190 # 4.75 seconds + # 2 => @stop_count > 144 # 3.6 seconds + # 3 => @stop_count > 102 # 2.55 seconds + # 4 => @stop_count > 64 # 1.6 seconds + # 5 => @stop_count > 30 # 0.75 seconds + # 6 => @stop_count > 0 # 0 seconds + if @stop_count >= self.move_frequency_real + case @move_type + when 1 then + move_type_random + when 2 then + move_type_toward_player + when 3 then + move_type_custom + end + end + end + + def update_move + # Move the character (the 0.1 catches rounding errors) + distance = (jumping?) ? jump_speed_real : move_speed_real + dest_x = @x * Game_Map::REAL_RES_X + dest_y = @y * Game_Map::REAL_RES_Y + if @real_x < dest_x + @real_x += distance + @real_x = dest_x if @real_x > dest_x - 0.1 + else + @real_x -= distance + @real_x = dest_x if @real_x < dest_x + 0.1 + end + if @real_y < dest_y + @real_y += distance + @real_y = dest_y if @real_y > dest_y - 0.1 + else + @real_y -= distance + @real_y = dest_y if @real_y < dest_y + 0.1 + end + # Refresh how far is left to travel in a jump + if jumping? + @jump_count -= 1 if @jump_count > 0 # For stationary jumps only + @jump_distance_left = [(dest_x - @real_x).abs, (dest_y - @real_y).abs].max + end + # End of a step, so perform events that happen at this time + if !jumping? && !moving? + Events.onStepTakenFieldMovement.trigger(self, self) + calculate_bush_depth + @stopped_this_frame = true + elsif !@moved_last_frame || @stopped_last_frame # Started a new step + calculate_bush_depth + @stopped_this_frame = false + end + # Increment animation counter + @anime_count += 1 if @walk_anime || @step_anime + @moved_this_frame = true + end + + def update_stop + @anime_count += 1 if @step_anime + @stop_count += 1 if !@starting && !lock? + @moved_this_frame = false + @stopped_this_frame = false + end + + def update_pattern + return if @lock_pattern + # return if @jump_count > 0 # Don't animate if jumping on the spot + # Character has stopped moving, return to original pattern + if @moved_last_frame && !@moved_this_frame && !@step_anime + @pattern = @original_pattern + @anime_count = 0 + return + end + # Character has started to move, change pattern immediately + if !@moved_last_frame && @moved_this_frame && !@step_anime + @pattern = (@pattern + 1) % 4 if @walk_anime + @anime_count = 0 + return + end + # Calculate how many frames each pattern should display for, i.e. the time + # it takes to move half a tile (or a whole tile if cycling). We assume the + # game uses square tiles. + real_speed = (jumping?) ? jump_speed_real : move_speed_real + frames_per_pattern = Game_Map::REAL_RES_X / (real_speed * 2.0) + frames_per_pattern *= 2 if move_speed >= 5 # Cycling speed or faster + return if @anime_count < frames_per_pattern + # Advance to the next animation frame + @pattern = (@pattern + 1) % 4 + @anime_count -= frames_per_pattern + end +end diff --git a/Data/Scripts/004_Game classes/008_Game_Event.rb b/Data/Scripts/004_Game classes/008_Game_Event.rb new file mode 100644 index 000000000..50d67abfc --- /dev/null +++ b/Data/Scripts/004_Game classes/008_Game_Event.rb @@ -0,0 +1,289 @@ +class Game_Event < Game_Character + attr_reader :map_id + attr_reader :trigger + attr_reader :list + attr_reader :starting + attr_reader :tempSwitches # Temporary self-switches + attr_accessor :need_refresh + + def initialize(map_id, event, map=nil) + super(map) + @map_id = map_id + @event = event + @id = @event.id + @original_x = @event.x + @original_y = @event.y + if @event.name[/size\((\d+),(\d+)\)/i] + @width = $~[1].to_i + @height = $~[2].to_i + end + @erased = false + @starting = false + @need_refresh = false + @route_erased = false + @through = true + @to_update = true + @tempSwitches = {} + if @event.name[/forced_z\s*=\s*(-?\d+)/i] + @forced_z = $1.to_i + end + moveto(@event.x, @event.y) if map + refresh + end + + def id; return @event.id; end + def name; return @event.name; end + + def set_starting + @starting = true + end + + def clear_starting + @starting = false + end + + def start + @starting = true if @list.size > 1 + end + + def erase + @erased = true + refresh + end + + def erase_route + @route_erased = true + refresh + end + + def tsOn?(c) + return @tempSwitches && @tempSwitches[c]==true + end + + def tsOff?(c) + return !@tempSwitches || !@tempSwitches[c] + end + + def setTempSwitchOn(c) + @tempSwitches[c]=true + refresh + end + + def setTempSwitchOff(c) + @tempSwitches[c]=false + refresh + end + + def isOff?(c) + return !$game_self_switches[[@map_id,@event.id,c]] + end + + def switchIsOn?(id) + switchname = $data_system.switches[id] + return false if !switchname + if switchname[/^s\:/] + return eval($~.post_match) + else + return $game_switches[id] + end + end + + def variable + return nil if !$PokemonGlobal.eventvars + return $PokemonGlobal.eventvars[[@map_id,@event.id]] + end + + def setVariable(variable) + $PokemonGlobal.eventvars[[@map_id,@event.id]]=variable + end + + def varAsInt + return 0 if !$PokemonGlobal.eventvars + return $PokemonGlobal.eventvars[[@map_id,@event.id]].to_i + end + + def expired?(secs=86400) + ontime=self.variable + time=pbGetTimeNow + return ontime && (time.to_i>ontime+secs) + end + + def expiredDays?(days=1) + ontime=self.variable.to_i + return false if !ontime + now=pbGetTimeNow + elapsed=(now.to_i-ontime)/86400 + elapsed+=1 if (now.to_i-ontime)%86400>(now.hour*3600+now.min*60+now.sec) + return elapsed>=days + end + + def cooledDown?(seconds) + return true if expired?(seconds) && tsOff?("A") + self.need_refresh = true + return false + end + + def cooledDownDays?(days) + return true if expiredDays?(days) && tsOff?("A") + self.need_refresh = true + return false + end + + def onEvent? + return @map_id == $game_map.map_id && at_coordinate?($game_player.x, $game_player.y) + end + + + def over_trigger? + return false if @character_name != "" && !@through + return false if @event.name[/hiddenitem/i] + each_occupied_tile do |i, j| + return true if self.map.passable?(i, j, 0, $game_player) + end + return false + end + + + def pbCheckEventTriggerAfterTurning + return if $game_system.map_interpreter.running? || @starting + if @event.name[/trainer\((\d+)\)/i] + distance = $~[1].to_i + if @trigger==2 && pbEventCanReachPlayer?(self,$game_player,distance) + start if !jumping? && !over_trigger? + end + end + end + + def check_event_trigger_touch(dir) + return if $game_system.map_interpreter.running? + return if @trigger != 2 # Event touch + case dir + when 2 + return if $game_player.y != @y + 1 + when 4 + return if $game_player.x != @x - 1 + when 6 + return if $game_player.x != @x + @width + when 8 + return if $game_player.y != @y - @height + end + return if !in_line_with_coordinate?($game_player.x, $game_player.y) + return if jumping? || over_trigger? + start + end + + def check_event_trigger_auto + if @trigger == 2 # Event touch + if at_coordinate?($game_player.x, $game_player.y) + start if !jumping? && over_trigger? + end + elsif @trigger == 3 # Autorun + start + end + end + + def refresh + new_page = nil + unless @erased + for page in @event.pages.reverse + c = page.condition + next if c.switch1_valid && !switchIsOn?(c.switch1_id) + next if c.switch2_valid && !switchIsOn?(c.switch2_id) + next if c.variable_valid && $game_variables[c.variable_id] < c.variable_value + if c.self_switch_valid + key = [@map_id, @event.id, c.self_switch_ch] + next if $game_self_switches[key] != true + end + new_page = page + break + end + end + return if new_page == @page + @page = new_page + clear_starting + if @page == nil + @tile_id = 0 + @character_name = "" + @character_hue = 0 + @move_type = 0 + @through = true + @trigger = nil + @list = nil + @interpreter = nil + return + end + @tile_id = @page.graphic.tile_id + @character_name = @page.graphic.character_name + @character_hue = @page.graphic.character_hue + if @original_direction != @page.graphic.direction + @direction = @page.graphic.direction + @original_direction = @direction + @prelock_direction = 0 + end + if @original_pattern != @page.graphic.pattern + @pattern = @page.graphic.pattern + @original_pattern = @pattern + end + @opacity = @page.graphic.opacity + @blend_type = @page.graphic.blend_type + @move_type = @page.move_type + self.move_speed = @page.move_speed + self.move_frequency = @page.move_frequency + @move_route = (@route_erased) ? RPG::MoveRoute.new : @page.move_route + @move_route_index = 0 + @move_route_forcing = false + @walk_anime = @page.walk_anime + @step_anime = @page.step_anime + @direction_fix = @page.direction_fix + @through = @page.through + @always_on_top = @page.always_on_top + calculate_bush_depth + @trigger = @page.trigger + @list = @page.list + @interpreter = nil + if @trigger == 4 # Parallel Process + @interpreter = Interpreter.new + end + check_event_trigger_auto + end + + def should_update?(recalc=false) + return @to_update if !recalc + return true if @trigger && (@trigger == 3 || @trigger == 4) + return true if @move_route_forcing + return true if @event.name[/update/i] + range = 2 # Number of tiles + return false if self.screen_x - @sprite_size[0]/2 > Graphics.width + range * Game_Map::TILE_WIDTH + return false if self.screen_x + @sprite_size[0]/2 < -range * Game_Map::TILE_WIDTH + return false if self.screen_y_ground - @sprite_size[1] > Graphics.height + range * Game_Map::TILE_HEIGHT + return false if self.screen_y_ground < -range * Game_Map::TILE_HEIGHT + return true + end + + def update + @to_update = should_update?(true) + return if !@to_update + last_moving = moving? + super + if !moving? && last_moving + $game_player.pbCheckEventTriggerFromDistance([2]) + end + if @need_refresh + @need_refresh = false + refresh + end + check_event_trigger_auto + if @interpreter != nil + unless @interpreter.running? + @interpreter.setup(@list, @event.id, @map_id) + end + @interpreter.update + end + end + + def active? + return !@erased && @page != nil + end + + +end diff --git a/Data/Scripts/004_Game classes/009_Game_Player.rb b/Data/Scripts/004_Game classes/009_Game_Player.rb new file mode 100644 index 000000000..b94ebcf87 --- /dev/null +++ b/Data/Scripts/004_Game classes/009_Game_Player.rb @@ -0,0 +1,447 @@ +#=============================================================================== +# ** Game_Player +#------------------------------------------------------------------------------- +# This class handles the player. Its functions include event starting +# determinants and map scrolling. Refer to "$game_player" for the one +# instance of this class. +#=============================================================================== +class Game_Player < Game_Character + attr_accessor :bump_se + attr_accessor :charsetData + attr_accessor :encounter_count + attr_accessor :x + attr_accessor :y + + SCREEN_CENTER_X = (Settings::SCREEN_WIDTH / 2 - Game_Map::TILE_WIDTH / 2) * Game_Map::X_SUBPIXELS + SCREEN_CENTER_Y = (Settings::SCREEN_HEIGHT / 2 - Game_Map::TILE_HEIGHT / 2) * Game_Map::Y_SUBPIXELS + + def initialize(*arg) + super(*arg) + @lastdir=0 + @lastdirframe=0 + @bump_se=0 + end + + def map + @map = nil + return $game_map + end + + def pbHasDependentEvents? + return $PokemonGlobal.dependentEvents.length>0 + end + + def bump_into_object + return if @bump_se && @bump_se>0 + pbSEPlay("Player bump") + @bump_se = Graphics.frame_rate/2 + end + + def move_generic(dir, turn_enabled = true) + turn_generic(dir, true) if turn_enabled + if !$PokemonTemp.encounterTriggered + if can_move_in_direction?(dir) + x_offset = (dir == 4) ? -1 : (dir == 6) ? 1 : 0 + y_offset = (dir == 8) ? -1 : (dir == 2) ? 1 : 0 + return if pbLedge(x_offset, y_offset) + return if pbEndSurf(x_offset, y_offset) + turn_generic(dir, true) + if !$PokemonTemp.encounterTriggered + @x += x_offset + @y += y_offset + $PokemonTemp.dependentEvents.pbMoveDependentEvents + increase_steps + end + elsif !check_event_trigger_touch(dir) + bump_into_object + end + end + $PokemonTemp.encounterTriggered = false + end + + def turn_generic(dir, keep_enc_indicator = false) + old_direction = @direction + super(dir) + if @direction != old_direction && !@move_route_forcing && !pbMapInterpreterRunning? + Events.onChangeDirection.trigger(self, self) + $PokemonTemp.encounterTriggered = false if !keep_enc_indicator + end + end + + def pbTriggeredTrainerEvents(triggers,checkIfRunning=true) + result = [] + # If event is running + return result if checkIfRunning && $game_system.map_interpreter.running? + # All event loops + for event in $game_map.events.values + next if !event.name[/trainer\((\d+)\)/i] + distance = $~[1].to_i + # If event coordinates and triggers are consistent + if pbEventCanReachPlayer?(event,self,distance) && triggers.include?(event.trigger) + # If starting determinant is front event (other than jumping) + result.push(event) if !event.jumping? && !event.over_trigger? + end + end + return result + end + + def pbTriggeredCounterEvents(triggers,checkIfRunning=true) + result = [] + # If event is running + return result if checkIfRunning && $game_system.map_interpreter.running? + # All event loops + for event in $game_map.events.values + next if !event.name[/counter\((\d+)\)/i] + distance = $~[1].to_i + # If event coordinates and triggers are consistent + if pbEventFacesPlayer?(event,self,distance) && triggers.include?(event.trigger) + # If starting determinant is front event (other than jumping) + result.push(event) if !event.jumping? && !event.over_trigger? + end + end + return result + end + + def pbCheckEventTriggerAfterTurning; end + + def pbCheckEventTriggerFromDistance(triggers) + ret = pbTriggeredTrainerEvents(triggers) + ret.concat(pbTriggeredCounterEvents(triggers)) + return false if ret.length==0 + for event in ret + event.start + end + return true + end + + def pbTerrainTag(countBridge = false) + return $MapFactory.getTerrainTag(self.map.map_id, @x, @y, countBridge) if $MapFactory + return $game_map.terrain_tag(@x, @y, countBridge) + end + + def pbFacingEvent(ignoreInterpreter=false) + return nil if $game_system.map_interpreter.running? && !ignoreInterpreter + # Check the tile in front of the player for events + new_x = @x + (@direction == 6 ? 1 : @direction == 4 ? -1 : 0) + new_y = @y + (@direction == 2 ? 1 : @direction == 8 ? -1 : 0) + + return nil if !$game_map.valid?(new_x, new_y) + + for event in $game_map.events.values + next if !event.at_coordinate?(new_x, new_y) + next if event.jumping? || event.over_trigger? + return event + end + # If the tile in front is a counter, check one tile beyond that for events + if $game_map.counter?(new_x, new_y) + new_x += (@direction == 6 ? 1 : @direction == 4 ? -1 : 0) + new_y += (@direction == 2 ? 1 : @direction == 8 ? -1 : 0) + for event in $game_map.events.values + next if !event.at_coordinate?(new_x, new_y) + next if event.jumping? || event.over_trigger? + return event + end + end + return nil + end + + + + #----------------------------------------------------------------------------- + # * Passable Determinants + # x : x-coordinate + # y : y-coordinate + # d : direction (0,2,4,6,8) + # * 0 = Determines if all directions are impassable (for jumping) + #----------------------------------------------------------------------------- + def passable?(x, y, d, strict = false) + # Get new coordinates + new_x = x + (d == 6 ? 1 : d == 4 ? -1 : 0) + new_y = y + (d == 2 ? 1 : d == 8 ? -1 : 0) + # If coordinates are outside of map + return false if !$game_map.validLax?(new_x, new_y) + if !$game_map.valid?(new_x, new_y) + return false if !$MapFactory + return $MapFactory.isPassableFromEdge?(new_x, new_y) + end + # If debug mode is ON and Ctrl key was pressed + return true if $DEBUG && Input.press?(Input::CTRL) + return super + end + + #----------------------------------------------------------------------------- + # * Set Map Display Position to Center of Screen + #----------------------------------------------------------------------------- + def center(x, y) + self.map.display_x = x * Game_Map::REAL_RES_X - SCREEN_CENTER_X + self.map.display_y = y * Game_Map::REAL_RES_Y - SCREEN_CENTER_Y + end + + + def isCentered() + x_centered = self.map.display_x == x * Game_Map::REAL_RES_X - SCREEN_CENTER_X + y_centered = self.map.display_y == y * Game_Map::REAL_RES_Y - SCREEN_CENTER_Y + return x_centered && y_centered + end + #----------------------------------------------------------------------------- + # * Move to Designated Position + # x : x-coordinate + # y : y-coordinate + #----------------------------------------------------------------------------- + def moveto(x, y) + super + # Centering + center(x, y) + # Make encounter count + make_encounter_count + end + + #----------------------------------------------------------------------------- + # * Make Encounter Count + #----------------------------------------------------------------------------- + def make_encounter_count + # Image of two dice rolling + if $game_map.map_id != 0 + n = $game_map.encounter_step + @encounter_count = rand(n) + rand(n) + 1 + end + end + + #----------------------------------------------------------------------------- + # * Refresh + #----------------------------------------------------------------------------- + def refresh + @opacity = 255 + @blend_type = 0 + end + + #----------------------------------------------------------------------------- + # * Trigger event(s) at the same coordinates as self with the appropriate + # trigger(s) that can be triggered + #----------------------------------------------------------------------------- + def check_event_trigger_here(triggers) + result = false + # If event is running + return result if $game_system.map_interpreter.running? + # All event loops + for event in $game_map.events.values + # If event coordinates and triggers are consistent + next if !event.at_coordinate?(@x, @y) + next if !triggers.include?(event.trigger) + # If starting determinant is same position event (other than jumping) + next if event.jumping? || !event.over_trigger? + event.start + result = true + end + return result + end + + #----------------------------------------------------------------------------- + # * Front Event Starting Determinant + #----------------------------------------------------------------------------- + def check_event_trigger_there(triggers) + result = false + # If event is running + return result if $game_system.map_interpreter.running? + # Calculate front event coordinates + new_x = @x + (@direction == 6 ? 1 : @direction == 4 ? -1 : 0) + new_y = @y + (@direction == 2 ? 1 : @direction == 8 ? -1 : 0) + return false if !$game_map.valid?(new_x, new_y) + # All event loops + for event in $game_map.events.values + # If event coordinates and triggers are consistent + next if !event.at_coordinate?(new_x, new_y) + next if !triggers.include?(event.trigger) + # If starting determinant is front event (other than jumping) + next if event.jumping? || event.over_trigger? + event.start + result = true + end + # If fitting event is not found + if result == false + # If front tile is a counter + if $game_map.counter?(new_x, new_y) + # Calculate coordinates of 1 tile further away + new_x += (@direction == 6 ? 1 : @direction == 4 ? -1 : 0) + new_y += (@direction == 2 ? 1 : @direction == 8 ? -1 : 0) + return false if !$game_map.valid?(new_x, new_y) + # All event loops + for event in $game_map.events.values + # If event coordinates and triggers are consistent + next if !event.at_coordinate?(new_x, new_y) + next if !triggers.include?(event.trigger) + # If starting determinant is front event (other than jumping) + next if event.jumping? || event.over_trigger? + event.start + result = true + end + end + end + return result + end + + #----------------------------------------------------------------------------- + # * Touch Event Starting Determinant + #----------------------------------------------------------------------------- + def check_event_trigger_touch(dir) + result = false + return result if $game_system.map_interpreter.running? + # All event loops + x_offset = (dir == 4) ? -1 : (dir == 6) ? 1 : 0 + y_offset = (dir == 8) ? -1 : (dir == 2) ? 1 : 0 + for event in $game_map.events.values + next if ![1, 2].include?(event.trigger) # Player touch, event touch + # If event coordinates and triggers are consistent + next if !event.at_coordinate?(@x + x_offset, @y + y_offset) + if event.name[/trainer\((\d+)\)/i] + distance = $~[1].to_i + next if !pbEventCanReachPlayer?(event,self,distance) + elsif event.name[/counter\((\d+)\)/i] + distance = $~[1].to_i + next if !pbEventFacesPlayer?(event,self,distance) + end + # If starting determinant is front event (other than jumping) + next if event.jumping? || event.over_trigger? + event.start + result = true + end + return result + end + + #----------------------------------------------------------------------------- + # * Frame Update + #----------------------------------------------------------------------------- + def update + last_real_x = @real_x + last_real_y = @real_y + super + update_screen_position(last_real_x, last_real_y) + # Update dependent events + $PokemonTemp.dependentEvents.updateDependentEvents + # Count down the time between allowed bump sounds + @bump_se -= 1 if @bump_se && @bump_se>0 + # Finish up dismounting from surfing + if $PokemonTemp.endSurf && !moving? + pbCancelVehicles + $PokemonTemp.surfJump = nil + $PokemonTemp.endSurf = false + end + update_event_triggering + end + + def update_command_new + dir = Input.dir4 + unless pbMapInterpreterRunning? || $game_temp.message_window_showing || + $PokemonTemp.miniupdate || $game_temp.in_menu + # Move player in the direction the directional button is being pressed + if @moved_last_frame || + (dir > 0 && dir == @lastdir && Graphics.frame_count - @lastdirframe > Graphics.frame_rate / 20) + case dir + when 2 then move_down + when 4 then move_left + when 6 then move_right + when 8 then move_up + end + elsif dir != @lastdir + case dir + when 2 then turn_down + when 4 then turn_left + when 6 then turn_right + when 8 then turn_up + end + end + end + # Record last direction input + @lastdirframe = Graphics.frame_count if dir != @lastdir + @lastdir = dir + end + + # Center player on-screen + def update_screen_position(last_real_x, last_real_y) + return if self.map.scrolling? || !(@moved_last_frame || @moved_this_frame) + self.map.display_x = @real_x - SCREEN_CENTER_X + self.map.display_y = @real_y - SCREEN_CENTER_Y + end + + def update_event_triggering + return if moving? + # Try triggering events upon walking into them/in front of them + if @moved_this_frame + $PokemonTemp.dependentEvents.pbTurnDependentEvents + result = pbCheckEventTriggerFromDistance([2]) + # Event determinant is via touch of same position event + result |= check_event_trigger_here([1,2]) + # No events triggered, try other event triggers upon finishing a step + pbOnStepTaken(result) + end + # Try to manually interact with events + if Input.trigger?(Input::USE) && !$PokemonTemp.miniupdate + # Same position and front event determinant + check_event_trigger_here([0]) + check_event_trigger_there([0,2]) + end + end +end + + + +def pbGetPlayerCharset(meta,charset,trainer=nil,force=false) + trainer = $Trainer if !trainer + outfit = (trainer) ? trainer.outfit : 0 + if $game_player && $game_player.charsetData && !force + return nil if $game_player.charsetData[0] == $Trainer.character_ID && + $game_player.charsetData[1] == charset && + $game_player.charsetData[2] == outfit + end + $game_player.charsetData = [$Trainer.character_ID,charset,outfit] if $game_player + ret = meta[charset] + ret = meta[1] if nil_or_empty?(ret) + # if pbResolveBitmap("Graphics/Characters/player/"+ret+"_"+outfit.to_s) + # ret = ret+"_"+outfit.to_s + # end + return ret +end + +def pbUpdateVehicle + meta = GameData::Metadata.get_player($Trainer.character_ID) + if meta + charset = 1 # Regular graphic + if $PokemonGlobal.diving; charset = 5 # Diving graphic + elsif $PokemonGlobal.surfing; charset = 3 # Surfing graphic + elsif $PokemonGlobal.bicycle; charset = 2 # Bicycle graphic + end + newCharName = pbGetPlayerCharset(meta,charset) + $game_player.character_name = newCharName if newCharName + end +end + +def pbCancelVehicles(destination=nil) + $PokemonGlobal.surfing = false + $PokemonGlobal.diving = false + $PokemonGlobal.bicycle = false if !destination || !pbCanUseBike?(destination) + pbUpdateVehicle +end + +def pbCanUseBike?(map_id) + map_metadata = GameData::MapMetadata.try_get(map_id) + return false if !map_metadata + return true if map_metadata.always_bicycle + val = map_metadata.can_bicycle || map_metadata.outdoor_map + return (val) ? true : false +end + +def pbMountBike + return if $PokemonGlobal.bicycle + $PokemonGlobal.bicycle = true + pbUpdateVehicle + bike_bgm = GameData::Metadata.get.bicycle_BGM + pbCueBGM(bike_bgm, 0.5) if bike_bgm + pbPokeRadarCancel +end + +def pbDismountBike + return if !$PokemonGlobal.bicycle + $PokemonGlobal.bicycle = false + pbUpdateVehicle + $game_map.autoplayAsCue +end diff --git a/Data/Scripts/004_Game classes/010_Game_Player_Visuals.rb b/Data/Scripts/004_Game classes/010_Game_Player_Visuals.rb new file mode 100644 index 000000000..dc11a491d --- /dev/null +++ b/Data/Scripts/004_Game classes/010_Game_Player_Visuals.rb @@ -0,0 +1,126 @@ +class Game_Player < Game_Character + @@bobFrameSpeed = 1.0/15 + + def fullPattern + case self.direction + when 2 then return self.pattern + when 4 then return self.pattern + 4 + when 6 then return self.pattern + 8 + when 8 then return self.pattern + 12 + end + return 0 + end + + def setDefaultCharName(chname,pattern,lockpattern=false) + return if pattern<0 || pattern>=16 + @defaultCharacterName = chname + @direction = [2,4,6,8][pattern/4] + @pattern = pattern%4 + @lock_pattern = lockpattern + end + + def pbCanRun? + return false if $game_temp.in_menu || $game_temp.in_battle || + @move_route_forcing || $game_temp.message_window_showing || + pbMapInterpreterRunning? + input = ($PokemonSystem.runstyle == 1) ^ Input.press?(Input::ACTION) + return input && $Trainer.has_running_shoes && !jumping? && + !$PokemonGlobal.diving && !$PokemonGlobal.surfing && + !$PokemonGlobal.bicycle && !$game_player.pbTerrainTag.must_walk + end + + def pbIsRunning? + return moving? && !@move_route_forcing && pbCanRun? + end + + def character_name + @defaultCharacterName = "" if !@defaultCharacterName + return @defaultCharacterName if @defaultCharacterName!="" + if !@move_route_forcing && $Trainer.character_ID>=0 + meta = GameData::Metadata.get_player($Trainer.character_ID) + if meta && !$PokemonGlobal.bicycle && !$PokemonGlobal.diving && !$PokemonGlobal.surfing + charset = 1 # Display normal character sprite + + + player_is_moving = moving? + if pbCanRun? && (player_is_moving || @wasmoving) && Input.dir4!=0 && meta[4] && meta[4]!="" + charset = 4 # Display running character sprite + end + + newCharName = pbGetPlayerCharset(meta,charset) + + if newCharName + # echoln caller + # echoln newCharName + # echoln "moving: " + moving?.to_s + # echoln "was moving: " + @wasmoving.to_s + # + # echoln "can run: " + pbCanRun?.to_s + # echoln "Input.dir4 " + Input.dir4.to_s + # + # + # echoln (moving? || @wasmoving) + # echoln charset + # echoln "" + end + + + + + @character_name = newCharName if newCharName + @wasmoving = player_is_moving + end + end + return @character_name + end + + def update_command + self.move_speed = 0.5 if $game_switches[SWITCH_SUPER_SLOW_SPEED] + if $game_player.pbTerrainTag.ice + self.move_speed = 4 # Sliding on ice + elsif !moving? && !@move_route_forcing && $PokemonGlobal + if $PokemonGlobal.bicycle + self.move_speed = $game_switches[SWITCH_RACE_BIKE] && !Input.press?(Input::ACTION) ? 5.5 : 5 # Cycling + elsif pbCanRun? || $PokemonGlobal.surfing + self.move_speed = 4 # Running, surfing + else + self.move_speed = 3 # Walking, diving + end + end + super + end + + def update_pattern + if $PokemonGlobal.surfing || $PokemonGlobal.diving + p = ((Graphics.frame_count%60)*@@bobFrameSpeed).floor + @pattern = p if !@lock_pattern + @pattern_surf = p + @bob_height = (p>=2) ? 2 : 0 + else + @bob_height = 0 + super + end + end +end + + +=begin +class Game_Character + alias update_old2 update + + def update + if self.is_a?(Game_Event) + if @dependentEvents + for i in 0...@dependentEvents.length + if @dependentEvents[i][0]==$game_map.map_id && + @dependentEvents[i][1]==self.id + self.move_speed_real = $game_player.move_speed_real + break + end + end + end + end + update_old2 + end +end +=end diff --git a/Data/Scripts/004_Game classes/011_Game_CommonEvent.rb b/Data/Scripts/004_Game classes/011_Game_CommonEvent.rb new file mode 100644 index 000000000..194f61519 --- /dev/null +++ b/Data/Scripts/004_Game classes/011_Game_CommonEvent.rb @@ -0,0 +1,82 @@ +#=============================================================================== +# ** Game_CommonEvent +#------------------------------------------------------------------------------- +# This class handles common events. It includes execution of parallel process +# event. This class is used within the Game_Map class ($game_map). +#=============================================================================== +class Game_CommonEvent + #----------------------------------------------------------------------------- + # * Object Initialization + # common_event_id : common event ID + #----------------------------------------------------------------------------- + def initialize(common_event_id) + @common_event_id = common_event_id + + @interpreter = nil + refresh + end + #----------------------------------------------------------------------------- + # * Get Name + #----------------------------------------------------------------------------- + def name + return $data_common_events[@common_event_id].name + end + #----------------------------------------------------------------------------- + # * Get Trigger + #----------------------------------------------------------------------------- + def trigger + return $data_common_events[@common_event_id].trigger + end + #----------------------------------------------------------------------------- + # * Get Condition Switch ID + #----------------------------------------------------------------------------- + def switch_id + return $data_common_events[@common_event_id].switch_id + end + #----------------------------------------------------------------------------- + # * Get List of Event Commands + #----------------------------------------------------------------------------- + def list + return $data_common_events[@common_event_id].list + end + #----------------------------------------------------------------------------- + # * Checks if switch is on + #----------------------------------------------------------------------------- + def switchIsOn?(id) + switchName = $data_system.switches[id] + return false if !switchName + if switchName[/^s\:/] + return eval($~.post_match) + else + return $game_switches[id] + end + end + #----------------------------------------------------------------------------- + # * Refresh + #----------------------------------------------------------------------------- + def refresh + # Create an interpreter for parallel process if necessary + if self.trigger == 2 && switchIsOn?(self.switch_id) + if @interpreter == nil + @interpreter = Interpreter.new + end + else + @interpreter = nil + end + end + #----------------------------------------------------------------------------- + # * Frame Update + #----------------------------------------------------------------------------- + def update + # If parallel process is valid + if @interpreter != nil + # If not running + unless @interpreter.running? + # Set up event + @interpreter.setup(self.list, 0) + end + # Update interpreter + @interpreter.update + end + end +end diff --git a/Data/Scripts/004_Game classes/012_Game_DependentEvents.rb b/Data/Scripts/004_Game classes/012_Game_DependentEvents.rb new file mode 100644 index 000000000..10dfb0cf3 --- /dev/null +++ b/Data/Scripts/004_Game classes/012_Game_DependentEvents.rb @@ -0,0 +1,588 @@ +class PokemonTemp + attr_writer :dependentEvents + + def dependentEvents + @dependentEvents = DependentEvents.new if !@dependentEvents + return @dependentEvents + end +end + + + +def pbRemoveDependencies() + $PokemonTemp.dependentEvents.removeAllEvents() + pbDeregisterPartner() rescue nil +end + +def pbAddDependency(event,follows=true) + $PokemonTemp.dependentEvents.addEvent(event) + $PokemonTemp.dependentEvents.follows_player = follows +end + +def pbRemoveDependency(event) + $PokemonTemp.dependentEvents.removeEvent(event) +end + +def pbAddDependency2(eventID, eventName, commonEvent) + $PokemonTemp.dependentEvents.addEvent($game_map.events[eventID],eventName,commonEvent) +end + +# Gets the Game_Character object associated with a dependent event. +def pbGetDependency(eventName) + return $PokemonTemp.dependentEvents.getEventByName(eventName) +end + +def pbRemoveDependency2(eventName) + $PokemonTemp.dependentEvents.removeEventByName(eventName) +end + + + +class PokemonGlobalMetadata + attr_writer :dependentEvents + + def dependentEvents + @dependentEvents = [] if !@dependentEvents + return @dependentEvents + end +end + + + +def pbTestPass(follower,x,y,_direction=nil) + return $MapFactory.isPassableStrict?(follower.map.map_id,x,y,follower) +end + +# Same map only +def moveThrough(follower,direction) + oldThrough=follower.through + follower.through=true + case direction + when 2 then follower.move_down + when 4 then follower.move_left + when 6 then follower.move_right + when 8 then follower.move_up + end + follower.through=oldThrough +end + +# Same map only +def moveFancy(follower,direction) + deltaX=(direction == 6 ? 1 : (direction == 4 ? -1 : 0)) + deltaY=(direction == 2 ? 1 : (direction == 8 ? -1 : 0)) + newX = follower.x + deltaX + newY = follower.y + deltaY + # Move if new position is the player's, or the new position is passable, + # or the current position is not passable + if ($game_player.x==newX && $game_player.y==newY) || + pbTestPass(follower,newX,newY,0) || + !pbTestPass(follower,follower.x,follower.y,0) + oldThrough=follower.through + follower.through=true + case direction + when 2 then follower.move_down + when 4 then follower.move_left + when 6 then follower.move_right + when 8 then follower.move_up + end + follower.through=oldThrough + end +end + +# Same map only +def jumpFancy(follower,direction,leader) + deltaX=(direction == 6 ? 2 : (direction == 4 ? -2 : 0)) + deltaY=(direction == 2 ? 2 : (direction == 8 ? -2 : 0)) + halfDeltaX=(direction == 6 ? 1 : (direction == 4 ? -1 : 0)) + halfDeltaY=(direction == 2 ? 1 : (direction == 8 ? -1 : 0)) + middle=pbTestPass(follower,follower.x+halfDeltaX,follower.y+halfDeltaY,0) + ending=pbTestPass(follower,follower.x+deltaX, follower.y+deltaY, 0) + if middle + moveFancy(follower,direction) + moveFancy(follower,direction) + elsif ending + if pbTestPass(follower,follower.x,follower.y,0) + if leader.jumping? + follower.jump_speed_real = leader.jump_speed_real * Graphics.frame_rate / 40.0 + else + follower.jump_speed_real = leader.move_speed_real * Graphics.frame_rate / 20.0 + end + follower.jump(deltaX,deltaY) + else + moveThrough(follower,direction) + moveThrough(follower,direction) + end + end +end + +def pbFancyMoveTo(follower,newX,newY,leader) + if follower.x-newX==-1 && follower.y==newY + moveFancy(follower,6) + elsif follower.x-newX==1 && follower.y==newY + moveFancy(follower,4) + elsif follower.y-newY==-1 && follower.x==newX + moveFancy(follower,2) + elsif follower.y-newY==1 && follower.x==newX + moveFancy(follower,8) + elsif follower.x-newX==-2 && follower.y==newY + jumpFancy(follower,6,leader) + elsif follower.x-newX==2 && follower.y==newY + jumpFancy(follower,4,leader) + elsif follower.y-newY==-2 && follower.x==newX + jumpFancy(follower,2,leader) + elsif follower.y-newY==2 && follower.x==newX + jumpFancy(follower,8,leader) + elsif follower.x!=newX || follower.y!=newY + follower.moveto(newX,newY) + end +end + + + +class DependentEvents + attr_reader :lastUpdate + attr_reader :realEvents + attr_writer :follows_player + + def createEvent(eventData) + rpgEvent = RPG::Event.new(eventData[3],eventData[4]) + rpgEvent.id = eventData[1] + if eventData[9] + # Must setup common event list here and now + commonEvent = Game_CommonEvent.new(eventData[9]) + rpgEvent.pages[0].list = commonEvent.list + end + newEvent = Game_Event.new(eventData[0],rpgEvent,$MapFactory.getMap(eventData[2])) + newEvent.character_name = eventData[6] + newEvent.character_hue = eventData[7] + case eventData[5] # direction + when 2 then newEvent.turn_down + when 4 then newEvent.turn_left + when 6 then newEvent.turn_right + when 8 then newEvent.turn_up + end + return newEvent + end + + def initialize + # Original map, Event ID, Current map, X, Y, Direction + events=$PokemonGlobal.dependentEvents + @realEvents=[] + @lastUpdate=-1 + for event in events + @realEvents.push(createEvent(event)) + end + @follows_player=true + end + + def pbEnsureEvent(event, newMapID) + events=$PokemonGlobal.dependentEvents + for i in 0...events.length + # Check original map ID and original event ID + if events[i][0]==event.map_id && events[i][1]==event.id + # Change current map ID + events[i][2]=newMapID + newEvent=createEvent(events[i]) + # Replace event + @realEvents[i]=newEvent + @lastUpdate+=1 + return i + end + end + return -1 + end + + def pbFollowEventAcrossMaps(leader,follower,instant=false,leaderIsTrueLeader=true) + d=leader.direction + areConnected=$MapFactory.areConnected?(leader.map.map_id,follower.map.map_id) + # Get the rear facing tile of leader + facingDirection=10-d + if !leaderIsTrueLeader && areConnected + relativePos=$MapFactory.getThisAndOtherEventRelativePos(leader,follower) + # Assumes leader and follower are both 1x1 tile in size + if (relativePos[1]==0 && relativePos[0]==2) # 2 spaces to the right of leader + facingDirection=6 + elsif (relativePos[1]==0 && relativePos[0]==-2) # 2 spaces to the left of leader + facingDirection=4 + elsif relativePos[1]==-2 && relativePos[0]==0 # 2 spaces above leader + facingDirection=8 + elsif relativePos[1]==2 && relativePos[0]==0 # 2 spaces below leader + facingDirection=2 + end + end + facings=[facingDirection] # Get facing from behind +# facings.push([0,0,4,0,8,0,2,0,6][d]) # Get right facing +# facings.push([0,0,6,0,2,0,8,0,4][d]) # Get left facing + if !leaderIsTrueLeader + facings.push(d) # Get forward facing + end + mapTile=nil + + if areConnected + bestRelativePos=-1 + oldthrough=follower.through + follower.through=false + for i in 0...facings.length + facing=facings[i] + tile=$MapFactory.getFacingTile(facing,leader) + # Assumes leader is 1x1 tile in size + passable=tile && $MapFactory.isPassableStrict?(tile[0],tile[1],tile[2],follower) + if i==0 && !passable && tile && + $MapFactory.getTerrainTag(tile[0],tile[1],tile[2]).ledge + # If the tile isn't passable and the tile is a ledge, + # get tile from further behind + tile=$MapFactory.getFacingTileFromPos(tile[0],tile[1],tile[2],facing) + passable=tile && $MapFactory.isPassableStrict?(tile[0],tile[1],tile[2],follower) + end + if passable + relativePos=$MapFactory.getThisAndOtherPosRelativePos( + follower,tile[0],tile[1],tile[2]) + # Assumes follower is 1x1 tile in size + distance=Math.sqrt(relativePos[0]*relativePos[0]+relativePos[1]*relativePos[1]) + if bestRelativePos==-1 || bestRelativePos>distance + bestRelativePos=distance + mapTile=tile + end + if i==0 && distance<=1 # Prefer behind if tile can move up to 1 space + break + end + end + end + follower.through=oldthrough + else + tile=$MapFactory.getFacingTile(facings[0],leader) + # Assumes leader is 1x1 tile in size + passable=tile && $MapFactory.isPassableStrict?(tile[0],tile[1],tile[2],follower) + mapTile=passable ? mapTile : nil + end + if mapTile && follower.map.map_id==mapTile[0] + return if !@follows_player + # Follower is on same map + newX=mapTile[1] + newY=mapTile[2] + deltaX=(d == 6 ? -1 : d == 4 ? 1 : 0) + deltaY=(d == 2 ? -1 : d == 8 ? 1 : 0) + posX = newX + deltaX + posY = newY + deltaY + + # if !@follows_player + # posX=follower.original_x + # posY=follower.original_y + # + # end + + follower.move_speed=leader.move_speed # sync movespeed + if (follower.x-newX==-1 && follower.y==newY) || + (follower.x-newX==1 && follower.y==newY) || + (follower.y-newY==-1 && follower.x==newX) || + (follower.y-newY==1 && follower.x==newX) + if instant + follower.moveto(newX,newY) + else + pbFancyMoveTo(follower,newX,newY,leader) + end + elsif (follower.x-newX==-2 && follower.y==newY) || + (follower.x-newX==2 && follower.y==newY) || + (follower.y-newY==-2 && follower.x==newX) || + (follower.y-newY==2 && follower.x==newX) + if instant + follower.moveto(newX,newY) + else + pbFancyMoveTo(follower,newX,newY,leader) + end + elsif follower.x!=posX || follower.y!=posY + if instant + follower.moveto(newX,newY) + else + pbFancyMoveTo(follower,posX,posY,leader) + pbFancyMoveTo(follower,newX,newY,leader) + end + end + else + if !mapTile + # Make current position into leader's position + mapTile=[leader.map.map_id,leader.x,leader.y] + end + if follower.map.map_id==mapTile[0] + # Follower is on same map as leader + follower.moveto(leader.x,leader.y) + else + # Follower will move to different map + events=$PokemonGlobal.dependentEvents + eventIndex=pbEnsureEvent(follower,mapTile[0]) + if eventIndex>=0 + newFollower=@realEvents[eventIndex] + newEventData=events[eventIndex] + newFollower.moveto(mapTile[1],mapTile[2]) + newEventData[3]=mapTile[1] + newEventData[4]=mapTile[2] + end + end + end + end + + def debugEcho + self.eachEvent { |e,d| + echoln d + echoln [e.map_id,e.map.map_id,e.id] + } + end + + def pbMapChangeMoveDependentEvents + events=$PokemonGlobal.dependentEvents + updateDependentEvents + leader=$game_player + for i in 0...events.length + event=@realEvents[i] + pbFollowEventAcrossMaps(leader,event,true,i==0) + # Update X and Y for this event + events[i][3]=event.x + events[i][4]=event.y + events[i][5]=event.direction + # Set leader to this event + leader=event + end + end + + def pbMoveDependentEvents + events=$PokemonGlobal.dependentEvents + updateDependentEvents + leader=$game_player + for i in 0...events.length + event=@realEvents[i] + if !@follows_player + pbRemoveDependencies if leader.map.map_id != event.map.map_id + pbFollowEventAcrossMaps(leader,event,false,i==0) + events[i][3]=event.original_x + events[i][4]=event.original_y + else + pbFollowEventAcrossMaps(leader,event,false,i==0) + # Update X and Y for this event + events[i][3]=event.x + events[i][4]=event.y + events[i][5]=event.direction + # Set leader to this event + leader=event + end + end + end + + def pbTurnDependentEvents + events=$PokemonGlobal.dependentEvents + updateDependentEvents + leader=$game_player + for i in 0...events.length + event=@realEvents[i] + pbTurnTowardEvent(event,leader) + # Update direction for this event + events[i][5]=event.direction + # Set leader to this event + leader=event + end + end + + def eachEvent + events=$PokemonGlobal.dependentEvents + for i in 0...events.length + yield @realEvents[i],events[i] + end + end + + def updateDependentEvents + events=$PokemonGlobal.dependentEvents + return if events.length==0 + for i in 0...events.length + break if !@follows_player + + event=@realEvents[i] + next if !@realEvents[i] + event.transparent=$game_player.transparent + if event.jumping? || event.moving? || + !($game_player.jumping? || $game_player.moving?) + event.update + elsif !event.starting + event.set_starting + event.update + event.clear_starting + end + events[i][3]=event.x + events[i][4]=event.y + events[i][5]=event.direction + end + # Check event triggers + # + if Input.trigger?(Input::USE) && !$game_temp.in_menu && !$game_temp.in_battle && + !$game_player.move_route_forcing && !$game_temp.message_window_showing && + !pbMapInterpreterRunning? + # Get position of tile facing the player + facingTile=$MapFactory.getFacingTile() + # Assumes player is 1x1 tile in size + self.eachEvent { |e,d| + next if !d[9] + if e.at_coordinate?($game_player.x, $game_player.y) + # On same position + if !e.jumping? && (!e.respond_to?("over_trigger") || e.over_trigger?) + if e.list.size>1 + # Start event + $game_map.refresh if $game_map.need_refresh + e.lock + pbMapInterpreter.setup(e.list,e.id,e.map.map_id) + end + end + elsif facingTile && e.map.map_id==facingTile[0] && + e.at_coordinate?(facingTile[1], facingTile[2]) + # On facing tile + if !e.jumping? && (!e.respond_to?("over_trigger") || !e.over_trigger?) + if e.list.size>1 + # Start event + $game_map.refresh if $game_map.need_refresh + e.lock + pbMapInterpreter.setup(e.list,e.id,e.map.map_id) + end + end + end + } + end + end + + def removeEvent(event) + events=$PokemonGlobal.dependentEvents + mapid=$game_map.map_id + for i in 0...events.length + if events[i][2]==mapid && # Refer to current map + events[i][0]==event.map_id && # Event's map ID is original ID + events[i][1]==event.id + events[i]=nil + @realEvents[i]=nil + @lastUpdate+=1 + end + end + events.compact! + @realEvents.compact! + end + + def getEventByName(name) + events=$PokemonGlobal.dependentEvents + for i in 0...events.length + if events[i] && events[i][8]==name # Arbitrary name given to dependent event + return @realEvents[i] + end + end + return nil + end + + def removeAllEvents + events=$PokemonGlobal.dependentEvents + events.clear + @realEvents.clear + @lastUpdate+=1 + end + + def removeEventByName(name) + events=$PokemonGlobal.dependentEvents + for i in 0...events.length + if events[i] && events[i][8]==name # Arbitrary name given to dependent event + events[i]=nil + @realEvents[i]=nil + @lastUpdate+=1 + end + end + events.compact! + @realEvents.compact! + end + + def addEvent(event,eventName=nil,commonEvent=nil) + return if !event + events=$PokemonGlobal.dependentEvents + + + for i in 0...events.length + if events[i] && events[i][0]==$game_map.map_id && events[i][1]==event.id + # Already exists + return + end + end + # Original map ID, original event ID, current map ID, + # event X, event Y, event direction, + # event's filename, + # event's hue, event's name, common event ID + eventData=[ + $game_map.map_id,event.id,$game_map.map_id, + event.x,event.y,event.direction, + event.character_name.clone, + event.character_hue,eventName,commonEvent + ] + newEvent=createEvent(eventData) + events.push(eventData) + @realEvents.push(newEvent) + @lastUpdate+=1 + event.erase + end +end + + + +class DependentEventSprites + def initialize(viewport,map) + @disposed=false + @sprites=[] + @map=map + @viewport=viewport + refresh + @lastUpdate=nil + end + + def refresh + for sprite in @sprites + sprite.dispose + end + @sprites.clear + $PokemonTemp.dependentEvents.eachEvent { |event,data| + if data[0]==@map.map_id # Check original map + @map.events[data[1]].erase + end + if data[2]==@map.map_id # Check current map + @sprites.push(Sprite_Character.new(@viewport,event)) + end + } + end + + def update + if $PokemonTemp.dependentEvents.lastUpdate!=@lastUpdate + refresh + @lastUpdate=$PokemonTemp.dependentEvents.lastUpdate + end + for sprite in @sprites + sprite.update + end + end + + def dispose + return if @disposed + for sprite in @sprites + sprite.dispose + end + @sprites.clear + @disposed=true + end + + def disposed? + @disposed + end +end + + + +Events.onSpritesetCreate += proc { |_sender,e| + spriteset = e[0] # Spriteset being created + viewport = e[1] # Viewport used for tilemap and characters + map = spriteset.map # Map associated with the spriteset (not necessarily the current map) + spriteset.addUserSprite(DependentEventSprites.new(viewport,map)) +} + +Events.onMapSceneChange += proc { |_sender,e| + mapChanged = e[1] + if mapChanged + $PokemonTemp.dependentEvents.pbMapChangeMoveDependentEvents + end +} diff --git a/Data/Scripts/005_Map renderer/001_Tilemap_XP.rb b/Data/Scripts/005_Map renderer/001_Tilemap_XP.rb new file mode 100644 index 000000000..5f197cacf --- /dev/null +++ b/Data/Scripts/005_Map renderer/001_Tilemap_XP.rb @@ -0,0 +1,921 @@ +#=============================================================================== +# +#=============================================================================== +class CustomTilemapAutotiles + attr_accessor :changed + + def initialize + @changed = true + @tiles = [nil,nil,nil,nil,nil,nil,nil] + end + + def [](i) + return @tiles[i] + end + + def []=(i,value) + @tiles[i] = value + @changed = true + end +end + + + +#Console::setup_console +class CustomTilemapSprite < Sprite +end + + + +#=============================================================================== +# +#=============================================================================== +class CustomTilemap + attr_reader :tileset + attr_reader :autotiles + attr_reader :map_data + attr_reader :flash_data + attr_reader :priorities + attr_reader :terrain_tags + attr_reader :visible + attr_reader :viewport + attr_reader :graphicsWidth + attr_reader :graphicsHeight + attr_reader :ox + attr_reader :oy + attr_accessor :tone + attr_accessor :color + + Autotiles = [ + [ [27, 28, 33, 34], [ 5, 28, 33, 34], [27, 6, 33, 34], [ 5, 6, 33, 34], + [27, 28, 33, 12], [ 5, 28, 33, 12], [27, 6, 33, 12], [ 5, 6, 33, 12] ], + [ [27, 28, 11, 34], [ 5, 28, 11, 34], [27, 6, 11, 34], [ 5, 6, 11, 34], + [27, 28, 11, 12], [ 5, 28, 11, 12], [27, 6, 11, 12], [ 5, 6, 11, 12] ], + [ [25, 26, 31, 32], [25, 6, 31, 32], [25, 26, 31, 12], [25, 6, 31, 12], + [15, 16, 21, 22], [15, 16, 21, 12], [15, 16, 11, 22], [15, 16, 11, 12] ], + [ [29, 30, 35, 36], [29, 30, 11, 36], [ 5, 30, 35, 36], [ 5, 30, 11, 36], + [39, 40, 45, 46], [ 5, 40, 45, 46], [39, 6, 45, 46], [ 5, 6, 45, 46] ], + [ [25, 30, 31, 36], [15, 16, 45, 46], [13, 14, 19, 20], [13, 14, 19, 12], + [17, 18, 23, 24], [17, 18, 11, 24], [41, 42, 47, 48], [ 5, 42, 47, 48] ], + [ [37, 38, 43, 44], [37, 6, 43, 44], [13, 18, 19, 24], [13, 14, 43, 44], + [37, 42, 43, 48], [17, 18, 47, 48], [13, 18, 43, 48], [ 1, 2, 7, 8] ] + ] + Animated_Autotiles_Frames = 5*Graphics.frame_rate/20 # Frequency of updating animated autotiles + FlashOpacity = [100,90,80,70,80,90] + + def initialize(viewport) + @tileset = nil # Refers to Map Tileset Name + @autotiles = CustomTilemapAutotiles.new + @map_data = nil # Refers to 3D Array Of Tile Settings + @flash_data = nil # Refers to 3D Array of Tile Flashdata + @priorities = nil # Refers to Tileset Priorities + @terrain_tags = nil # Refers to Tileset Terrain Tags + @visible = true # Refers to Tileset Visibleness + @ox = 0 # Bitmap Offsets + @oy = 0 # Bitmap Offsets + @plane = false + @haveGraphicsWH = (Graphics.width!=nil rescue false) + if @haveGraphicsWH + @graphicsWidth = Graphics.width + @graphicsHeight = Graphics.height + else + @graphicsWidth = 640 + @graphicsHeight = 480 + end + @tileWidth = Game_Map::TILE_WIDTH rescue 32 + @tileHeight = Game_Map::TILE_HEIGHT rescue 32 + @tileSrcWidth = 32 + @tileSrcHeight = 32 + @diffsizes = (@tileWidth!=@tileSrcWidth) || (@tileHeight!=@tileSrcHeight) + @tone = Tone.new(0,0,0,0) + @oldtone = Tone.new(0,0,0,0) + @color = Color.new(0,0,0,0) + @oldcolor = Color.new(0,0,0,0) + @selfviewport = Viewport.new(0,0,graphicsWidth,graphicsHeight) + @viewport = (viewport) ? viewport : @selfviewport + @tiles = [] + @autotileInfo = [] + @regularTileInfo = [] + @oldOx = 0 + @oldOy = 0 + @oldViewportOx = 0 + @oldViewportOy = 0 + @layer0 = CustomTilemapSprite.new(viewport) + @layer0.visible = true + @nowshown = false + @layer0.bitmap = Bitmap.new([graphicsWidth+320,1].max,[graphicsHeight+320,1].max) + @layer0.z = 0 + @layer0.ox = 0 + @layer0.oy = 0 + @oxLayer0 = 0 + @oyLayer0 = 0 + @flash = nil + @oxFlash = 0 + @oyFlash = 0 + @priotiles = {} + @priotilesfast = [] + @prioautotiles = {} + @autosprites = [] + @framecount = [0,0,0,0,0,0,0,0] # For autotiles + @tilesetChanged = true + @flashChanged = false + @firsttime = true + @disposed = false + @usedsprites = false + @layer0clip = true + @firsttimeflash = true + @fullyrefreshed = false + @fullyrefreshedautos = false + end + + def dispose + return if disposed? + @help.dispose if @help + @help = nil + i = 0; len = @autotileInfo.length + while i=xsize + xEnd = (@ox+@viewport.rect.width)/@tileWidth + 1 + xEnd = 0 if xEnd<0 + xEnd = xsize-1 if xEnd>=xsize + return false if xStart>=xEnd + ysize = @map_data.ysize + yStart = @oy/@tileHeight - 1 + yStart = 0 if yStart<0 + yStart = ysize-1 if yStart>=ysize + yEnd = (@oy+@viewport.rect.height)/@tileHeight + 1 + yEnd = 0 if yEnd<0 + yEnd = ysize-1 if yEnd>=ysize + return false if yStart>=yEnd + return true + end + + def autotileNumFrames(id) + autotile = @autotiles[id/48-1] + return 0 if !autotile || autotile.disposed? + frames = 1 + if autotile.height==@tileHeight + frames = autotile.width/@tileWidth + else + frames = autotile.width/(3*@tileWidth) + end + return frames + end + + def autotileFrame(id) + autotile = @autotiles[id/48-1] + return -1 if !autotile || autotile.disposed? + frames = 1 + if autotile.height==@tileHeight + frames = autotile.width/@tileWidth + else + frames = autotile.width/(3*@tileWidth) + end + return (Graphics.frame_count/Animated_Autotiles_Frames)%frames + end + + def repaintAutotiles + for i in 0...@autotileInfo.length + next if !@autotileInfo[i] + frame = autotileFrame(i) + @autotileInfo[i].clear + bltAutotile(@autotileInfo[i],0,0,i,frame) + end + end + + def bltAutotile(bitmap,x,y,id,frame) + return if frame<0 + autotile = @autotiles[id/48-1] + return if !autotile || autotile.disposed? + if autotile.height==@tileSrcHeight + anim = frame*@tileSrcWidth + src_rect = Rect.new(anim,0,@tileSrcWidth,@tileSrcHeight) + if @diffsizes + bitmap.stretch_blt(Rect.new(x,y,@tileWidth,@tileHeight),autotile,src_rect) + else + bitmap.blt(x,y,autotile,src_rect) + end + else + anim = frame*3*@tileSrcWidth + id %= 48 + tiles = Autotiles[id>>3][id&7] + src = Rect.new(0,0,0,0) + halfTileWidth = @tileWidth>>1 + halfTileHeight = @tileHeight>>1 + halfTileSrcWidth = @tileSrcWidth>>1 + halfTileSrcHeight = @tileSrcHeight>>1 + for i in 0...4 + tile_position = tiles[i] - 1 + src.set( (tile_position % 6)*halfTileSrcWidth + anim, + (tile_position / 6)*halfTileSrcHeight, halfTileSrcWidth, halfTileSrcHeight) + if @diffsizes + bitmap.stretch_blt( + Rect.new(i%2*halfTileWidth+x,i/2*halfTileHeight+y,halfTileWidth,halfTileHeight), + autotile,src) + else + bitmap.blt(i%2*halfTileWidth+x,i/2*halfTileHeight+y, autotile, src) + end + end + end + end + + def getAutotile(sprite,id) + frames = @framecount[id/48-1] + if frames<=1 + anim = 0 + else + anim = (Graphics.frame_count/Animated_Autotiles_Frames)%frames + end + return if anim<0 + bitmap = @autotileInfo[id] + if !bitmap + bitmap = Bitmap.new(@tileWidth,@tileHeight) + bltAutotile(bitmap,0,0,id,anim) + @autotileInfo[id] = bitmap + end + sprite.bitmap = bitmap if sprite.bitmap!=bitmap + end + + def getRegularTile(sprite,id) + if @diffsizes + bitmap = @regularTileInfo[id] + if !bitmap + bitmap = Bitmap.new(@tileWidth,@tileHeight) + rect = Rect.new(((id - 384)&7)*@tileSrcWidth,((id - 384)>>3)*@tileSrcHeight, + @tileSrcWidth,@tileSrcHeight) + bitmap.stretch_blt(Rect.new(0,0,@tileWidth,@tileHeight),@tileset,rect) + @regularTileInfo[id] = bitmap + end + sprite.bitmap = bitmap if sprite.bitmap!=bitmap + else + sprite.bitmap = @tileset if sprite.bitmap!=@tileset + sprite.src_rect.set(((id - 384)&7)*@tileSrcWidth,((id - 384)>>3)*@tileSrcHeight, + @tileSrcWidth,@tileSrcHeight) + end + end + + def addTile(tiles,count,xpos,ypos,id) + terrain = @terrain_tags[id] + priority = @priorities[id] + if id >= 384 # Tileset tile + if count>=tiles.length + sprite = CustomTilemapSprite.new(@viewport) + tiles.push(sprite,0) + else + sprite = tiles[count] + tiles[count+1] = 0 + end + sprite.visible = @visible + sprite.x = xpos + sprite.y = ypos + sprite.tone = @tone + sprite.color = @color + getRegularTile(sprite,id) + else # Autotile + if count>=tiles.length + sprite = CustomTilemapSprite.new(@viewport) + tiles.push(sprite,1) + else + sprite = tiles[count] + tiles[count+1] = 1 + end + sprite.visible = @visible + sprite.x = xpos + sprite.y = ypos + sprite.tone = @tone + sprite.color = @color + getAutotile(sprite,id) + end + if PBTerrain.hasReflections?(terrain) + spriteZ = -100 + elsif $PokemonGlobal.bridge>0 && PBTerrain.isBridge?(terrain) + spriteZ = 1 + else + spriteZ = (priority==0) ? 0 : ypos+priority*32+32 + end + sprite.z = spriteZ + count += 2 + return count + end + + def refresh_flash + if @flash_data && !@flash + @flash = CustomTilemapSprite.new(viewport) + @flash.visible = true + @flash.z = 1 + @flash.tone = tone + @flash.color = color + @flash.blend_type = 1 + @flash.bitmap = Bitmap.new([graphicsWidth*2,1].max,[graphicsHeight*2,1].max) + @firsttimeflash = true + elsif !@flash_data && @flash + @flash.bitmap.dispose if @flash.bitmap + @flash.dispose + @flash = nil + @firsttimeflash = false + end + end + + def refreshFlashSprite + return if !@flash || @flash_data.nil? + ptX = @ox-@oxFlash + ptY = @oy-@oyFlash + if !@firsttimeflash && !@usedsprites && + ptX>=0 && ptX+@viewport.rect.width<=@flash.bitmap.width && + ptY>=0 && ptY+@viewport.rect.height<=@flash.bitmap.height + @flash.ox = 0 + @flash.oy = 0 + @flash.src_rect.set(ptX.round,ptY.round, + @viewport.rect.width,@viewport.rect.height) + return + end + width = @flash.bitmap.width + height = @flash.bitmap.height + bitmap = @flash.bitmap + ysize = @map_data.ysize + xsize = @map_data.xsize + @firsttimeflash = false + @oxFlash = @ox-(width>>2) + @oyFlash = @oy-(height>>2) + @flash.ox = 0 + @flash.oy = 0 + @flash.src_rect.set(width>>2,height>>2, + @viewport.rect.width,@viewport.rect.height) + @flash.bitmap.clear + @oxFlash = @oxFlash.floor + @oyFlash = @oyFlash.floor + xStart = @oxFlash/@tileWidth + xStart = 0 if xStart<0 + yStart = @oyFlash/@tileHeight + yStart = 0 if yStart<0 + xEnd = xStart+(width/@tileWidth)+1 + yEnd = yStart+(height/@tileHeight)+1 + xEnd = xsize if xEnd>=xsize + yEnd = ysize if yEnd>=ysize + if xStart>8)&15 + g = (id>>4)&15 + b = (id)&15 + tmpcolor.set(r<<4,g<<4,b<<4) + bitmap.fill_rect(xpos,ypos,@tileWidth,@tileHeight,tmpcolor) + end + end + end + end + + def refresh_tileset + i = 0 + len = @regularTileInfo.length + while i < len + if @regularTileInfo[i] + @regularTileInfo[i].dispose + @regularTileInfo[i] = nil + end + i += 1 + end + @regularTileInfo.clear + @priotiles.clear + ysize = @map_data.ysize + xsize = @map_data.xsize + zsize = @map_data.zsize + if xsize > 100 || ysize > 100 + @fullyrefreshed = false + else + for z in 0...zsize + for y in 0...ysize + for x in 0...xsize + id = @map_data[x, y, z] + next if id == 0 + next if @priorities[id] == 0 && !PBTerrain.hasReflections?(@terrain_tags[id]) + @priotiles[[x, y]] = [] if !@priotiles[[x, y]] + @priotiles[[x, y]].push([z, id]) + end + end + end + @fullyrefreshed = true + end + end + + def refresh_autotiles + i = 0 + len = @autotileInfo.length + while i < len + if @autotileInfo[i] + @autotileInfo[i].dispose + @autotileInfo[i] = nil + end + i += 1 + end + i = 0 + len = @autosprites.length + while i < len + if @autosprites[i] + @autosprites[i].dispose + @autosprites[i] = nil + end + i += 2 + end + @autosprites.clear + @autotileInfo.clear + @prioautotiles.clear + @priorect = nil + @priorectautos = nil + hasanimated = false + for i in 0...7 + numframes = autotileNumFrames(48 * (i + 1)) + hasanimated = true if numframes >= 2 + @framecount[i] = numframes + end + if hasanimated + ysize = @map_data.ysize + xsize = @map_data.xsize + zsize = @map_data.zsize + if xsize > 100 || ysize > 100 + @fullyrefreshedautos = false + else + for y in 0...ysize + for x in 0...xsize + for z in 0...zsize + id = @map_data[x, y, z] + next if id == 0 || id >= 384 # Skip non-autotiles + next if @priorities[id] != 0 || PBTerrain.hasReflections?(@terrain_tags[id]) + next if @framecount[id / 48 - 1] < 2 + @prioautotiles[[x, y]] = true + break + end + end + end + @fullyrefreshedautos = true + end + else + @fullyrefreshedautos = true + end + end + + def refreshLayer0(autotiles = false) + return true if autotiles && !shown? + ptX = @ox - @oxLayer0 + ptY = @oy - @oyLayer0 + if !autotiles && !@firsttime && !@usedsprites && + ptX >= 0 && ptX + @viewport.rect.width <= @layer0.bitmap.width && + ptY >= 0 && ptY + @viewport.rect.height <= @layer0.bitmap.height + if @layer0clip && @viewport.ox == 0 && @viewport.oy == 0 + @layer0.ox = 0 + @layer0.oy = 0 + @layer0.src_rect.set(ptX.round, ptY.round, @viewport.rect.width, @viewport.rect.height) + else + @layer0.ox = ptX.round + @layer0.oy = ptY.round + @layer0.src_rect.set(0, 0, @layer0.bitmap.width, @layer0.bitmap.height) + end + return true + end + width = @layer0.bitmap.width + height = @layer0.bitmap.height + bitmap = @layer0.bitmap + ysize = @map_data.ysize + xsize = @map_data.xsize + zsize = @map_data.zsize + twidth = @tileWidth + theight = @tileHeight + mapdata = @map_data + if autotiles + return true if @fullyrefreshedautos && @prioautotiles.length == 0 + xStart = @oxLayer0 / twidth + xStart = 0 if xStart < 0 + yStart = @oyLayer0 / theight + yStart = 0 if yStart < 0 + xEnd = xStart + (width / twidth) + 1 + xEnd = xsize if xEnd > xsize + yEnd = yStart + (height / theight) + 1 + yEnd = ysize if yEnd > ysize + return true if xStart >= xEnd || yStart >= yEnd + trans = Color.new(0, 0, 0, 0) + temprect = Rect.new(0, 0, 0, 0) + tilerect = Rect.new(0, 0, twidth, theight) + zrange = 0...zsize + overallcount = 0 + count = 0 + if !@fullyrefreshedautos + for y in yStart..yEnd + for x in xStart..xEnd + for z in zrange + id = mapdata[x, y, z] + next if !id || id < 48 || id >= 384 # Skip non-autotiles + prioid = @priorities[id] + next if prioid != 0 || PBTerrain.hasReflections?(@terrain_tags[id]) + fcount = @framecount[id / 48 - 1] + next if !fcount || fcount < 2 + overallcount += 1 + xpos = (x * twidth) - @oxLayer0 + ypos = (y * theight) - @oyLayer0 + bitmap.fill_rect(xpos, ypos, twidth, theight, trans) if overallcount <= 2000 + break + end + for z in zrange + id = mapdata[x, y, z] + next if !id || id < 48 + prioid = @priorities[id] + next if prioid != 0 || PBTerrain.hasReflections?(@terrain_tags[id]) + if overallcount > 2000 + xpos = (x * twidth) - @oxLayer0 + ypos = (y * theight) - @oyLayer0 + count = addTile(@autosprites, count, xpos, ypos, id) + elsif id >= 384 # Tileset tiles + temprect.set(((id - 384) & 7) * @tileSrcWidth, + ((id - 384) >> 3) * @tileSrcHeight, + @tileSrcWidth, @tileSrcHeight) + xpos = (x * twidth) - @oxLayer0 + ypos = (y * theight) - @oyLayer0 + if @diffsizes + bitmap.stretch_blt(Rect.new(xpos, ypos, twidth, theight), @tileset, temprect) + else + bitmap.blt(xpos, ypos, @tileset, temprect) + end + else # Autotiles + tilebitmap = @autotileInfo[id] + if !tilebitmap + anim = autotileFrame(id) + next if anim < 0 + tilebitmap = Bitmap.new(twidth, theight) + bltAutotile(tilebitmap, 0, 0, id, anim) + @autotileInfo[id] = tilebitmap + end + xpos = (x * twidth) - @oxLayer0 + ypos = (y * theight) - @oyLayer0 + bitmap.blt(xpos, ypos, tilebitmap, tilerect) + end + end + end + end + Graphics.frame_reset + else + if !@priorect || !@priorectautos || + @priorect[0] != xStart || @priorect[1] != yStart || + @priorect[2] != xEnd || @priorect[3] != yEnd + @priorect = [xStart, yStart, xEnd, yEnd] + @priorectautos = [] + for y in yStart..yEnd + for x in xStart..xEnd + @priorectautos.push([x, y]) if @prioautotiles[[x, y]] + end + end + end + for tile in @priorectautos + x = tile[0] + y = tile[1] + overallcount += 1 + xpos = (x * twidth) - @oxLayer0 + ypos = (y * theight) - @oyLayer0 + bitmap.fill_rect(xpos, ypos, twidth, theight, trans) + z = 0 + while z < zsize + id = mapdata[x, y, z] + z += 1 + next if !id || id < 48 + prioid = @priorities[id] + next if prioid != 0 || PBTerrain.hasReflections?(@terrain_tags[id]) + if id >= 384 # Tileset tiles + temprect.set(((id - 384) & 7) * @tileSrcWidth, + ((id - 384) >> 3) * @tileSrcHeight, + @tileSrcWidth, @tileSrcHeight) + if @diffsizes + bitmap.stretch_blt(Rect.new(xpos, ypos, twidth, theight), @tileset, temprect) + else + bitmap.blt(xpos, ypos, @tileset, temprect) + end + else # Autotiles + tilebitmap = @autotileInfo[id] + if !tilebitmap + anim = autotileFrame(id) + next if anim < 0 + tilebitmap = Bitmap.new(twidth, theight) + bltAutotile(tilebitmap, 0, 0, id, anim) + @autotileInfo[id] = tilebitmap + end + bitmap.blt(xpos, ypos, tilebitmap, tilerect) + end + end + end + Graphics.frame_reset if overallcount > 500 + end + @usedsprites = false + return true + end + return false if @usedsprites + @firsttime = false + @oxLayer0 = @ox - (width >> 2) + @oyLayer0 = @oy - (height >> 2) + if @layer0clip + @layer0.ox = 0 + @layer0.oy = 0 + @layer0.src_rect.set(width >> 2, height >> 2, @viewport.rect.width, @viewport.rect.height) + else + @layer0.ox = (width >> 2) + @layer0.oy = (height >> 2) + end + @layer0.bitmap.clear + @oxLayer0 = @oxLayer0.round + @oyLayer0 = @oyLayer0.round + xStart = @oxLayer0 / twidth + xStart = 0 if xStart < 0 + yStart = @oyLayer0 / theight + yStart = 0 if yStart < 0 + xEnd = xStart + (width / twidth) + 1 + xEnd = xsize if xEnd >= xsize + yEnd = yStart + (height / theight) + 1 + yEnd = ysize if yEnd >= ysize + if xStart < xEnd && yStart < yEnd + tmprect = Rect.new(0, 0, 0, 0) + yrange = yStart...yEnd + xrange = xStart...xEnd + for z in 0...zsize + for y in yrange + ypos = (y * theight) - @oyLayer0 + for x in xrange + xpos = (x * twidth) - @oxLayer0 + id = mapdata[x, y, z] + next if id == 0 || @priorities[id] != 0 || PBTerrain.hasReflections?(@terrain_tags[id]) + if id >= 384 # Tileset tiles + tmprect.set(((id - 384) & 7) * @tileSrcWidth, + ((id - 384) >> 3) * @tileSrcHeight, + @tileSrcWidth, @tileSrcHeight) + if @diffsizes + bitmap.stretch_blt(Rect.new(xpos, ypos, twidth, theight), @tileset, tmprect) + else + bitmap.blt(xpos, ypos, @tileset, tmprect) + end + else # Autotiles + frames = @framecount[id / 48 - 1] + if frames <= 1 + frame = 0 + else + frame = (Graphics.frame_count / Animated_Autotiles_Frames) % frames + end + bltAutotile(bitmap, xpos, ypos, id, frame) + end + end + end + end + Graphics.frame_reset + end + return true + end + + def refresh(autotiles = false) + @oldOx = @ox + @oldOy = @oy + usesprites = false + if @layer0 + @layer0.visible = @visible + usesprites = !refreshLayer0(autotiles) + return if autotiles && !usesprites + else + usesprites = true + end + refreshFlashSprite + xsize = @map_data.xsize + ysize = @map_data.ysize + minX = (@ox / @tileWidth) - 1 + minX.clamp(0, xsize - 1) + maxX = ((@ox + @viewport.rect.width) / @tileWidth) + 1 + maxX.clamp(0, xsize - 1) + minY = (@oy / @tileHeight) - 1 + minY.clamp(0, ysize - 1) + maxY = ((@oy + @viewport.rect.height) / @tileHeight) + 1 + maxY.clamp(0, ysize - 1) + count = 0 + if minX < maxX && minY < maxY + @usedsprites = usesprites || @usedsprites + @layer0.visible = false if usesprites && @layer0 + if !@priotilesrect || !@priotilesfast || + @priotilesrect[0] != minX || @priotilesrect[1] != minY || + @priotilesrect[2] != maxX || @priotilesrect[3] != maxY + @priotilesrect = [minX, minY, maxX, maxY] + @priotilesfast = [] + if @fullyrefreshed + for y in minY..maxY + for x in minX..maxX + next if !@priotiles[[x, y]] + @priotiles[[x, y]].each { |tile| @priotilesfast.push([x, y, tile[0], tile[1]]) } + end + end + else + for z in 0...@map_data.zsize + for y in minY..maxY + for x in minX..maxX + id = @map_data[x, y, z] + next if id == 0 + next if @priorities[id] == 0 && !PBTerrain.hasReflections?(@terrain_tags[id]) + @priotilesfast.push([x, y, z, id]) + end + end + end + end + end + for prio in @priotilesfast + xpos = (prio[0] * @tileWidth) - @ox + ypos = (prio[1] * @tileHeight) - @oy + count = addTile(@tiles, count, xpos, ypos, prio[3]) + end + end + if count < @tiles.length + bigchange = (count <= (@tiles.length * 2 / 3)) && @tiles.length > 40 + j = count + len = @tiles.length + while j < len + sprite = @tiles[j] + @tiles[j + 1] = -1 + if bigchange + sprite.dispose + @tiles[j] = nil + @tiles[j + 1] = nil + elsif !@tiles[j].disposed? + sprite.visible = false if sprite.visible + end + j += 2 + end + @tiles.compact! if bigchange + end + end + + def update + if @haveGraphicsWH + @graphicsWidth = Graphics.width + @graphicsHeight = Graphics.height + end + # Update tone + if @oldtone != @tone + @layer0.tone = @tone + @flash.tone = @tone if @flash + for sprite in @autosprites + sprite.tone = @tone if sprite.is_a?(Sprite) + end + for sprite in @tiles + sprite.tone = @tone if sprite.is_a?(Sprite) + end + @oldtone = @tone.clone + end + # Update color + if @oldcolor != @color + @layer0.color = @color + @flash.color = @color if @flash + for sprite in @autosprites + sprite.color = @color if sprite.is_a?(Sprite) + end + for sprite in @tiles + sprite.color = @color if sprite.is_a?(Sprite) + end + @oldcolor = @color.clone + end + # Refresh anything that has changed + if @autotiles.changed + refresh_autotiles + repaintAutotiles + end + refresh_flash if @flashChanged + refresh_tileset if @tilesetChanged + @flash.opacity = FlashOpacity[(Graphics.frame_count / 2) % 6] if @flash + mustrefresh = (@oldOx != @ox || @oldOy != @oy || @tilesetChanged || @autotiles.changed) + if @viewport.ox != @oldViewportOx || @viewport.oy != @oldViewportOy + mustrefresh = true + @oldViewportOx = @viewport.ox + @oldViewportOy = @viewport.oy + end + refresh if mustrefresh + if (Graphics.frame_count % Animated_Autotiles_Frames) == 0 || @nowshown + repaintAutotiles + refresh(true) + end + @nowshown = false + @autotiles.changed = false + @tilesetChanged = false + end +end diff --git a/Data/Scripts/005_Sprites/001_Sprite_Picture.rb b/Data/Scripts/005_Sprites/001_Sprite_Picture.rb new file mode 100644 index 000000000..47ed370eb --- /dev/null +++ b/Data/Scripts/005_Sprites/001_Sprite_Picture.rb @@ -0,0 +1,58 @@ +class Sprite_Picture + def initialize(viewport, picture) + @viewport = viewport + @picture = picture + @sprite = nil + update + end + + def dispose + @sprite.dispose if @sprite + end + + def update + @sprite.update if @sprite + # If picture file name is different from current one + if @picture_name != @picture.name + # Remember file name to instance variables + @picture_name = @picture.name + # If file name is not empty + if @picture_name != "" + # Get picture graphic + @sprite=IconSprite.new(0,0,@viewport) if !@sprite + @sprite.setBitmap("Graphics/Pictures/"+@picture_name) + end + end + # If file name is empty + if @picture_name == "" + # Set sprite to invisible + if @sprite + @sprite.dispose if @sprite + @sprite=nil + end + return + end + # Set sprite to visible + @sprite.visible = true + # Set transfer starting point + if @picture.origin == 0 + @sprite.ox = 0 + @sprite.oy = 0 + else + @sprite.ox = @sprite.bitmap.width / 2 + @sprite.oy = @sprite.bitmap.height / 2 + end + # Set sprite coordinates + @sprite.x = @picture.x + @sprite.y = @picture.y + @sprite.z = @picture.number + # Set zoom rate, opacity level, and blend method + @sprite.zoom_x = @picture.zoom_x / 100.0 + @sprite.zoom_y = @picture.zoom_y / 100.0 + @sprite.opacity = @picture.opacity + @sprite.blend_type = @picture.blend_type + # Set rotation angle and color tone + @sprite.angle = @picture.angle + @sprite.tone = @picture.tone + end +end diff --git a/Data/Scripts/005_Sprites/002_Sprite_Timer.rb b/Data/Scripts/005_Sprites/002_Sprite_Timer.rb new file mode 100644 index 000000000..4e5b26274 --- /dev/null +++ b/Data/Scripts/005_Sprites/002_Sprite_Timer.rb @@ -0,0 +1,45 @@ +class Sprite_Timer + def initialize(viewport=nil) + @viewport=viewport + @timer=nil + @total_sec=nil + @disposed=false + end + + def dispose + @timer.dispose if @timer + @timer=nil + @disposed=true + end + + def disposed? + @disposed + end + + def update + return if disposed? + if $game_system.timer_working + @timer.visible = true if @timer + if !@timer + @timer=Window_AdvancedTextPokemon.newWithSize("",Graphics.width-120,0,120,64) + @timer.width=@timer.borderX+96 + @timer.x=Graphics.width-@timer.width + @timer.viewport=@viewport + @timer.z=99998 + end + curtime=$game_system.timer / Graphics.frame_rate + curtime=0 if curtime<0 + if curtime != @total_sec + # Calculate total number of seconds + @total_sec = curtime + # Make a string for displaying the timer + min = @total_sec / 60 + sec = @total_sec % 60 + @timer.text = _ISPRINTF("{1:02d}:{2:02d}", min, sec) + end + @timer.update + else + @timer.visible=false if @timer + end + end +end diff --git a/Data/Scripts/005_Sprites/003_Sprite_Character.rb b/Data/Scripts/005_Sprites/003_Sprite_Character.rb new file mode 100644 index 000000000..83aedfc2c --- /dev/null +++ b/Data/Scripts/005_Sprites/003_Sprite_Character.rb @@ -0,0 +1,270 @@ +class BushBitmap + def initialize(bitmap, isTile, depth) + @bitmaps = [] + @bitmap = bitmap + @isTile = isTile + @isBitmap = @bitmap.is_a?(Bitmap) + @depth = depth + @manual_refresh = false + end + + def dispose + @bitmaps.each { |b| b.dispose if b } + end + + def bitmap + thisBitmap = (@isBitmap) ? @bitmap : @bitmap.bitmap + current = (@isBitmap) ? 0 : @bitmap.currentIndex + if !@bitmaps[current] + if @isTile + @bitmaps[current] = pbBushDepthTile(thisBitmap, @depth) + else + @bitmaps[current] = pbBushDepthBitmap(thisBitmap, @depth) + end + end + return @bitmaps[current] + end + + def pbBushDepthBitmap(bitmap, depth) + ret = Bitmap.new(bitmap.width, bitmap.height) + charheight = ret.height / 4 + cy = charheight - depth - 2 + for i in 0...4 + y = i * charheight + if cy >= 0 + ret.blt(0, y, bitmap, Rect.new(0, y, ret.width, cy)) + ret.blt(0, y + cy, bitmap, Rect.new(0, y + cy, ret.width, 2), 170) + end + ret.blt(0, y + cy + 2, bitmap, Rect.new(0, y + cy + 2, ret.width, 2), 85) if cy + 2 >= 0 + end + return ret + end + + def pbBushDepthTile(bitmap, depth) + ret = Bitmap.new(bitmap.width, bitmap.height) + charheight = ret.height + cy = charheight - depth - 2 + y = charheight + if cy >= 0 + ret.blt(0, y, bitmap, Rect.new(0, y, ret.width, cy)) + ret.blt(0, y + cy, bitmap, Rect.new(0, y + cy, ret.width, 2), 170) + end + ret.blt(0, y + cy + 2, bitmap, Rect.new(0, y + cy + 2, ret.width, 2), 85) if cy + 2 >= 0 + return ret + end +end + +def event_is_trainer(event) + return $game_map.events[event.id] && event.name[/trainer\((\d+)\)/i] +end + +class Sprite_Character < RPG::Sprite + attr_accessor :character + attr_accessor :pending_bitmap + attr_accessor :bitmap_override + attr_accessor :charbitmap + + def initialize(viewport, character = nil) + super(viewport) + @character = character + if darknessEffectOnCurrentMap() + if @character.is_a?(Game_Event) + $game_map.events[@character.id].erase if event_is_trainer(@character) + end + end + + @oldbushdepth = 0 + @spriteoffset = false + if Settings::USE_REFLECTIONS && (!character || character == $game_player || (character.name[/reflection/i] rescue false)) + @reflection = Sprite_Reflection.new(self, viewport) + end + @surfbase = Sprite_SurfBase.new(self, character, viewport) if character == $game_player + if @character && @character != $game_player + checkModifySpriteGraphics(@character) if @character.active? + end + update + end + + def event_is_active?(game_event) + return !game_event.event.page.nil? + end + + def checkModifySpriteGraphics(character) + return if character == $game_player || !character.name + if TYPE_EXPERTS_APPEARANCES.keys.include?(character.name.to_sym) + typeExpert = character.name.to_sym + setSpriteToAppearance(TYPE_EXPERTS_APPEARANCES[typeExpert]) + end + end + + def setSpriteToAppearance(trainerAppearance) + #return if !@charbitmap || !@charbitmap.bitmap + begin + new_bitmap = AnimatedBitmap.new(getBaseOverworldSpriteFilename()) #@charbitmap + new_bitmap.bitmap = generateNPCClothedBitmapStatic(trainerAppearance) + @bitmap_override = new_bitmap + updateBitmap + rescue + end + end + + def clearBitmapOverride() + @bitmap_override = nil + updateBitmap + end + + def setSurfingPokemon(pokemonSpecies) + @surfingPokemon = pokemonSpecies + @surfbase.setPokemon(pokemonSpecies) if @surfbase + end + + def groundY + return @character.screen_y_ground + end + + def visible=(value) + super(value) + @reflection.visible = value if @reflection + end + + def dispose + @bushbitmap.dispose if @bushbitmap + @bushbitmap = nil + @charbitmap.dispose if @charbitmap + @charbitmap = nil + @reflection.dispose if @reflection + @reflection = nil + @surfbase.dispose if @surfbase + @surfbase = nil + super + end + + def updateBitmap + @manual_refresh = true + end + + def pbLoadOutfitBitmap(outfitFileName) + # Construct the file path for the outfit bitmap based on the given value + #outfitFileName = sprintf("Graphics/Outfits/%s", value) + + # Attempt to load the outfit bitmap + begin + outfitBitmap = RPG::Cache.load_bitmap("", outfitFileName) + return outfitBitmap + rescue + return nil + end + end + + def generateClothedBitmap() + return + end + + def applyDayNightTone() + if @character.is_a?(Game_Event) && @character.name[/regulartone/i] + self.tone.set(0, 0, 0, 0) + else + pbDayNightTint(self) + end + end + + def updateCharacterBitmap + AnimatedBitmap.new('Graphics/Characters/' + @character_name, @character_hue) + end + + def should_update? + return @tile_id != @character.tile_id || + @character_name != @character.character_name || + @character_hue != @character.character_hue || + @oldbushdepth != @character.bush_depth || + @manual_refresh + end + + def refreshOutfit() + self.pending_bitmap = getClothedPlayerSprite(true) + end + + def update + if self.pending_bitmap + self.bitmap = self.pending_bitmap + self.pending_bitmap = nil + end + return if @character.is_a?(Game_Event) && !@character.should_update? + super + if should_update? + @manual_refresh = false + @tile_id = @character.tile_id + @character_name = @character.character_name + @character_hue = @character.character_hue + @oldbushdepth = @character.bush_depth + if @tile_id >= 384 + @charbitmap.dispose if @charbitmap + @charbitmap = pbGetTileBitmap(@character.map.tileset_name, @tile_id, + @character_hue, @character.width, @character.height) + @charbitmapAnimated = false + @bushbitmap.dispose if @bushbitmap + @bushbitmap = nil + @spriteoffset = false + @cw = Game_Map::TILE_WIDTH * @character.width + @ch = Game_Map::TILE_HEIGHT * @character.height + self.src_rect.set(0, 0, @cw, @ch) + self.ox = @cw / 2 + self.oy = @ch + @character.sprite_size = [@cw, @ch] + else + @charbitmap.dispose if @charbitmap + + @charbitmap = updateCharacterBitmap() + @charbitmap = @bitmap_override.clone if @bitmap_override + + RPG::Cache.retain('Graphics/Characters/', @character_name, @character_hue) if @charbitmapAnimated = true + @bushbitmap.dispose if @bushbitmap + @bushbitmap = nil + #@spriteoffset = @character_name[/offset/i] + @spriteoffset = @character_name[/fish/i] || @character_name[/dive/i] || @character_name[/surf/i] + @cw = @charbitmap.width / 4 if !@charbitmap.disposed? + @ch = @charbitmap.height / 4 if !@charbitmap.disposed? + self.ox = @cw / 2 + @character.sprite_size = [@cw, @ch] + end + end + @charbitmap.update if @charbitmapAnimated + bushdepth = @character.bush_depth + if bushdepth == 0 + if @character == $game_player + self.bitmap = getClothedPlayerSprite() #generateClothedBitmap() + else + self.bitmap = (@charbitmapAnimated) ? @charbitmap.bitmap : @charbitmap + end + else + @bushbitmap = BushBitmap.new(@charbitmap, (@tile_id >= 384), bushdepth) if !@bushbitmap + self.bitmap = @bushbitmap.bitmap + end + self.visible = !@character.transparent + if @tile_id == 0 + sx = @character.pattern * @cw + sy = ((@character.direction - 2) / 2) * @ch + self.src_rect.set(sx, sy, @cw, @ch) + self.oy = (@spriteoffset rescue false) ? @ch - 16 : @ch + self.oy -= @character.bob_height + end + if self.visible + applyDayNightTone() + end + self.x = @character.screen_x + self.y = @character.screen_y + self.z = @character.screen_z(@ch) + # self.zoom_x = Game_Map::TILE_WIDTH / 32.0 + # self.zoom_y = Game_Map::TILE_HEIGHT / 32.0 + self.opacity = @character.opacity + self.blend_type = @character.blend_type + # self.bush_depth = @character.bush_depth + if @character.animation_id != 0 + animation = $data_animations[@character.animation_id] + animation(animation, true) + @character.animation_id = 0 + end + @reflection.update if @reflection + @surfbase.update if @surfbase + end +end diff --git a/Data/Scripts/005_Sprites/004_Sprite_Reflection.rb b/Data/Scripts/005_Sprites/004_Sprite_Reflection.rb new file mode 100644 index 000000000..10e5c6c0e --- /dev/null +++ b/Data/Scripts/005_Sprites/004_Sprite_Reflection.rb @@ -0,0 +1,93 @@ +#=============================================================================== +# +#=============================================================================== +class Sprite_Reflection + attr_reader :visible + + def initialize(parent_sprite, viewport = nil) + @parent_sprite = parent_sprite + @sprite = nil + @height = 0 + @fixedheight = false + if @parent_sprite.character && @parent_sprite.character != $game_player && + @parent_sprite.character.name[/reflection\((\d+)\)/i] + @height = $~[1].to_i || 0 + @fixedheight = true + end + @viewport = viewport + @disposed = false + update + end + + def dispose + return if @disposed + @sprite&.dispose + @sprite = nil + @parent_sprite = nil + @disposed = true + end + + def disposed? + return @disposed + end + + def event + return @parent_sprite.character + end + + def visible=(value) + @visible = value + @sprite.visible = value if @sprite && !@sprite.disposed? + end + + def update + return if disposed? + shouldShow = @parent_sprite.visible + if !shouldShow + # Just-in-time disposal of sprite + if @sprite + @sprite.dispose + @sprite = nil + end + return + end + # Just-in-time creation of sprite + @sprite = Sprite.new(@viewport) if !@sprite + if @sprite + x = @parent_sprite.x - (@parent_sprite.ox * TilemapRenderer::ZOOM_X) + y = @parent_sprite.y - (@parent_sprite.oy * TilemapRenderer::ZOOM_Y) + y -= Game_Map::TILE_HEIGHT * TilemapRenderer::ZOOM_Y if event.character_name[/offset/i] + @height = $PokemonGlobal.bridge if !@fixedheight + y += @height * TilemapRenderer::ZOOM_Y * Game_Map::TILE_HEIGHT / 2 + width = @parent_sprite.src_rect.width + height = @parent_sprite.src_rect.height + @sprite.x = x + ((width / 2) * TilemapRenderer::ZOOM_X) + @sprite.y = y + ((height + (height / 2)) * TilemapRenderer::ZOOM_Y) + @sprite.ox = width / 2 + @sprite.oy = (height / 2) - 2 # Hard-coded 2 pixel shift up + @sprite.oy -= event.bob_height * 2 + @sprite.z = @parent_sprite.groundY - (Graphics.height / 2) + @sprite.z -= 1000 # Still water is -2000, map is 0 and above + @sprite.z += 1 if event == $game_player + @sprite.zoom_x = @parent_sprite.zoom_x + if Settings::ANIMATE_REFLECTIONS + @sprite.zoom_x += 0.05 * @sprite.zoom_x * Math.sin(2 * Math::PI * System.uptime) + end + @sprite.zoom_y = @parent_sprite.zoom_y + @sprite.angle = 180.0 + @sprite.mirror = true + @sprite.bitmap = @parent_sprite.bitmap + @sprite.tone = @parent_sprite.tone + if @height > 0 + @sprite.color = Color.new(48, 96, 160, 255) # Dark still water + @sprite.opacity = @parent_sprite.opacity + @sprite.visible = !Settings::TIME_SHADING # Can't time-tone a colored sprite + else + @sprite.color = Color.new(224, 224, 224, 96) + @sprite.opacity = @parent_sprite.opacity * 3 / 4 + @sprite.visible = true + end + @sprite.src_rect = @parent_sprite.src_rect + end + end +end diff --git a/Data/Scripts/005_Sprites/005_Sprite_SurfBase.rb b/Data/Scripts/005_Sprites/005_Sprite_SurfBase.rb new file mode 100644 index 000000000..cf2cb10ff --- /dev/null +++ b/Data/Scripts/005_Sprites/005_Sprite_SurfBase.rb @@ -0,0 +1,150 @@ +class Sprite_SurfBase + attr_reader :visible + attr_accessor :event + + def initialize(sprite, event, viewport = nil) + @rsprite = sprite + @sprite = nil + @event = event + @viewport = viewport + @disposed = false + #@surfbitmap = AnimatedBitmap.new("Graphics/Characters/base_surf") + @surfbitmap = update_surf_bitmap(:SURF) + @divebitmap = update_surf_bitmap(:DIVE) + # RPG::Cache.retain("Graphics/Characters/base_surf") + # RPG::Cache.retain("Graphics/Characters/base_dive") + @cws = @surfbitmap.width / 4 + @chs = @surfbitmap.height / 4 + @cwd = @divebitmap.width / 4 + @chd = @divebitmap.height / 4 + update + end + + def dispose + return if @disposed + @sprite.dispose if @sprite + @sprite = nil + @surfbitmap.dispose + @divebitmap.dispose + @disposed = true + end + + def disposed? + @disposed + end + + def visible=(value) + @visible = value + @sprite.visible = value if @sprite && !@sprite.disposed? + end + + def update_surf_bitmap(type) + species = $Trainer.surfing_pokemon + path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_board" if type == :SURF + #path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_scuba" if type == :DIVE + path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_Head" if type == :DIVE + if species + shape = species.shape + basePath = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + action = "divemon" if type == :DIVE + action = "surfmon" if type == :SURF + path = "#{basePath}#{action}_#{shape.to_s}" + end + return AnimatedBitmap.new(path) + end + + + # case species.shape + # when :Head + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_Head" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_Head" if type == :SURF + # when :Serpentine + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :Finned + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :HeadArms + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :HeadBase + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :BipedalTail + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :HeadLegs + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :Quadruped + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :Winged + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :Multiped + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :MultiBody + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :Bipedal + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :MultiWinged + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # when :Insectoid + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_HeadBase" if type == :DIVE + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "surfmon_HeadBase" if type == :SURF + # else + # path = Settings::PLAYER_GRAPHICS_FOLDER + Settings::PLAYER_SURFBASE_FOLDER + "divemon_01" + # end + + + def update + return if disposed? + if !$PokemonGlobal.surfing && !$PokemonGlobal.diving + # Just-in-time disposal of sprite + if @sprite + @sprite.dispose + @sprite = nil + end + return + end + # Just-in-time creation of sprite + @sprite = Sprite.new(@viewport) if !@sprite + if @sprite + if $PokemonGlobal.surfing + @surfbitmap = update_surf_bitmap(:SURF) + @sprite.bitmap = @surfbitmap.bitmap + cw = @cws + ch = @chs + elsif $PokemonGlobal.diving + @divebitmap = update_surf_bitmap(:DIVE) + @sprite.bitmap = @divebitmap.bitmap + cw = @cwd + ch = @chd + end + sx = @event.pattern_surf * cw + sy = ((@event.direction - 2) / 2) * ch + @sprite.src_rect.set(sx, sy, cw, ch) + if $PokemonTemp.surfJump + @sprite.x = ($PokemonTemp.surfJump[0] * Game_Map::REAL_RES_X - @event.map.display_x + 3) / 4 + (Game_Map::TILE_WIDTH / 2) + @sprite.y = ($PokemonTemp.surfJump[1] * Game_Map::REAL_RES_Y - @event.map.display_y + 3) / 4 + (Game_Map::TILE_HEIGHT / 2) + 16 + else + @sprite.x = @rsprite.x + @sprite.y = @rsprite.y + end + @sprite.ox = cw / 2 + @sprite.oy = ch - 16 # Assume base needs offsetting + @sprite.oy -= @event.bob_height + @sprite.z = @event.screen_z(ch) - 1 + @sprite.zoom_x = @rsprite.zoom_x + @sprite.zoom_y = @rsprite.zoom_y + @sprite.tone = @rsprite.tone + @sprite.color = @rsprite.color + @sprite.opacity = @rsprite.opacity + end + end +end diff --git a/Data/Scripts/005_Sprites/006_Spriteset_Global.rb b/Data/Scripts/005_Sprites/006_Spriteset_Global.rb new file mode 100644 index 000000000..c10b611c4 --- /dev/null +++ b/Data/Scripts/005_Sprites/006_Spriteset_Global.rb @@ -0,0 +1,30 @@ +class Spriteset_Global + attr_reader :playersprite + @@viewport2 = Viewport.new(0, 0, Settings::SCREEN_WIDTH, Settings::SCREEN_HEIGHT) + @@viewport2.z = 200 + + def initialize + @playersprite = Sprite_Player.new(Spriteset_Map.viewport, $game_player) + @picture_sprites = [] + for i in 1..100 + @picture_sprites.push(Sprite_Picture.new(@@viewport2, $game_screen.pictures[i])) + end + @timer_sprite = Sprite_Timer.new + update + end + + def dispose + @playersprite.dispose + @picture_sprites.each { |sprite| sprite.dispose } + @timer_sprite.dispose + @playersprite = nil + @picture_sprites.clear + @timer_sprite = nil + end + + def update + @playersprite.update + @picture_sprites.each { |sprite| sprite.update } + @timer_sprite.update + end +end diff --git a/Data/Scripts/005_Sprites/007_Spriteset_Map.rb b/Data/Scripts/005_Sprites/007_Spriteset_Map.rb new file mode 100644 index 000000000..06984f6a5 --- /dev/null +++ b/Data/Scripts/005_Sprites/007_Spriteset_Map.rb @@ -0,0 +1,168 @@ +# Unused +class ClippableSprite < Sprite_Character + def initialize(viewport,event,tilemap) + @tilemap = tilemap + @_src_rect = Rect.new(0,0,0,0) + super(viewport,event) + end + + def update + super + @_src_rect = self.src_rect + tmright = @tilemap.map_data.xsize*Game_Map::TILE_WIDTH-@tilemap.ox + echoln("x=#{self.x},ox=#{self.ox},tmright=#{tmright},tmox=#{@tilemap.ox}") + if @tilemap.ox-self.ox<-self.x + # clipped on left + diff = -self.x-@tilemap.ox+self.ox + self.src_rect = Rect.new(@_src_rect.x+diff,@_src_rect.y, + @_src_rect.width-diff,@_src_rect.height) + echoln("clipped out left: #{diff} #{@tilemap.ox-self.ox} #{self.x}") + elsif tmright-self.ox 1) ? params[1] : 0 + @anglemax = (params.size > 2) ? params[2] : 0 + @self_opacity = (params.size > 4) ? params[4] : 100 + @distancemax = (params.size > 3) ? params[3] : 350 + @character = character + update + end + + def dispose + @chbitmap&.dispose + super + end + + def update + if !in_range?(@character, @source, @distancemax) + self.opacity = 0 + return + end + super + if @tile_id != @character.tile_id || + @character_name != @character.character_name || + @character_hue != @character.character_hue + @tile_id = @character.tile_id + @character_name = @character.character_name + @character_hue = @character.character_hue + @chbitmap&.dispose + if @tile_id >= 384 + @chbitmap = pbGetTileBitmap(@character.map.tileset_name, + @tile_id, @character.character_hue) + self.src_rect.set(0, 0, 32, 32) + @ch = 32 + @cw = 32 + self.ox = 16 + self.oy = 32 + else + @chbitmap = AnimatedBitmap.new('Graphics/Characters/' + @character.character_name, + @character.character_hue) + @cw = @chbitmap.width / 4 + @ch = @chbitmap.height / 4 + self.ox = @cw / 2 + self.oy = @ch + end + end + if @chbitmap.is_a?(AnimatedBitmap) + @chbitmap.update + self.bitmap = @chbitmap.bitmap + else + self.bitmap = @chbitmap + end + self.visible = !@character.transparent + if @tile_id == 0 + sx = @character.pattern * @cw + sy = (@character.direction - 2) / 2 * @ch + if self.angle > 90 || angle < -90 + case @character.direction + when 2 then sy = @ch * 3 + when 4 then sy = @ch * 2 + when 6 then sy = @ch + when 8 then sy = 0 + end + end + self.src_rect.set(sx, sy, @cw, @ch) + end + self.x = ScreenPosHelper.pbScreenX(@character) + self.y = ScreenPosHelper.pbScreenY(@character) - 5 + self.z = ScreenPosHelper.pbScreenZ(@character, @ch) - 1 + self.zoom_x = ScreenPosHelper.pbScreenZoomX(@character) + self.zoom_y = ScreenPosHelper.pbScreenZoomY(@character) + self.blend_type = @character.blend_type + self.bush_depth = @character.bush_depth + if @character.animation_id != 0 + animation = $data_animations[@character.animation_id] + animation(animation, true) + @character.animation_id = 0 + end + @deltax = ScreenPosHelper.pbScreenX(@source) - self.x + @deltay = ScreenPosHelper.pbScreenY(@source) - self.y + self.color = Color.new(0, 0, 0) + @distance = ((@deltax**2) + (@deltay**2)) + self.opacity = @self_opacity * 13_000 / ((@distance * 370 / @distancemax) + 6000) + self.angle = 57.3 * Math.atan2(@deltax, @deltay) + @angle_trigo = self.angle + 90 + @angle_trigo += 360 if @angle_trigo < 0 + if @anglemin != 0 || @anglemax != 0 + if (@angle_trigo < @anglemin || @angle_trigo > @anglemax) && @anglemin < @anglemax + self.opacity = 0 + return + end + if @angle_trigo < @anglemin && @angle_trigo > @anglemax && @anglemin > @anglemax + self.opacity = 0 + return + end + end + end + + def in_range?(element, object, range) # From Near's Anti Lag Script, edited + elemScreenX = ScreenPosHelper.pbScreenX(element) + elemScreenY = ScreenPosHelper.pbScreenY(element) + objScreenX = ScreenPosHelper.pbScreenX(object) + objScreenY = ScreenPosHelper.pbScreenY(object) + x = (elemScreenX - objScreenX) * (elemScreenX - objScreenX) + y = (elemScreenY - objScreenY) * (elemScreenY - objScreenY) + r = x + y + return r <= range * range + end +end + + + +#=================================================== +# ? CLASS Sprite_Character edit +#=================================================== +class Sprite_Character < RPG::Sprite + alias shadow_initialize initialize unless method_defined?(:shadow_initialize) + + def initialize(viewport, character = nil) + @ombrelist = [] + @character = character + shadow_initialize(viewport, @character) + end + + def setShadows(map, shadows) + if character.is_a?(Game_Event) && shadows.length > 0 + params = XPML_read(map, "Shadow", @character, 4) + if params != nil + shadows.each do |shadow| + @ombrelist.push(Sprite_Shadow.new(viewport, @character, shadows)) + end + end + end + if character.is_a?(Game_Player) && shadows.length > 0 + shadows.each do |shadow| + @ombrelist.push(Sprite_Shadow.new(viewport, $game_player, shadow)) + end + end + update + end + + def clearShadows + @ombrelist.each { |s| s&.dispose } + @ombrelist.clear + end + + alias shadow_update update unless method_defined?(:shadow_update) + + def update + shadow_update + @ombrelist.each { |ombre| ombre.update } + end +end + + + +#=================================================== +# ? CLASS Game_Event edit +#=================================================== +class Game_Event + attr_accessor :id +end + + + +#=================================================== +# ? CLASS Spriteset_Map edit +#=================================================== +class Spriteset_Map + attr_accessor :shadows + + alias shadow_initialize initialize unless method_defined?(:shadow_initialize) + + def initialize(map = nil) + @shadows = [] + warn = false + map = $game_map if !map + map.events.keys.sort.each do |k| + ev = map.events[k] + warn = true if ev.list != nil && ev.list.length > 0 && ev.list[0].code == 108 && + (ev.list[0].parameters == ["s"] || ev.list[0].parameters == ["o"]) + params = XPML_read(map, "Shadow Source", ev, 4) + @shadows.push([ev] + params) if params != nil + end + if warn == true + p "Warning : At least one event on this map uses the obsolete way to add shadows" + end + shadow_initialize(map) + @character_sprites.each do |sprite| + sprite.setShadows(map, @shadows) + end + $scene.spritesetGlobal.playersprite.setShadows(map, @shadows) + end +end + + + +#=================================================== +# ? XPML Definition, by Rataime, using ideas from Near Fantastica +# +# Returns nil if the markup wasn't present at all, +# returns [] if there wasn't any parameters, else +# returns a parameters list with "int" converted as int +# eg : +# begin first +# begin second +# param1 1 +# param2 two +# begin third +# anything 3 +# +# p XPML_read("first", event_id) -> [] +# p XPML_read("second", event_id) -> [1, "two"] +# p XPML_read("third", event_id) -> [3] +# p XPML_read("forth", event_id) -> nil +#=================================================== +def XPML_read(map, markup, event, max_param_number = 0) + parameter_list = nil + return nil if !event || event.list.nil? + event.list.size.times do |i| + if event.list[i].code == 108 && + event.list[i].parameters[0].downcase == "begin " + markup.downcase + parameter_list = [] if parameter_list.nil? + ((i + 1)...event.list.size).each do |j| + if event.list[j].code == 108 + parts = event.list[j].parameters[0].split + if parts.size != 1 && parts[0].downcase != "begin" + if parts[1].to_i != 0 || parts[1] == "0" + parameter_list.push(parts[1].to_i) + else + parameter_list.push(parts[1]) + end + else + return parameter_list + end + else + return parameter_list + end + return parameter_list if max_param_number != 0 && j == i + max_param_number + end + end + end + return parameter_list +end diff --git a/Data/Scripts/005_Sprites/010_ParticleEngine.rb b/Data/Scripts/005_Sprites/010_ParticleEngine.rb new file mode 100644 index 000000000..01037b8ed --- /dev/null +++ b/Data/Scripts/005_Sprites/010_ParticleEngine.rb @@ -0,0 +1,586 @@ +# Particle Engine, Peter O., 2007-11-03 +# Based on version 2 by Near Fantastica, 04.01.06 +# In turn based on the Particle Engine designed by PinkMan +class Particle_Engine + def initialize(viewport = nil, map = nil) + @map = (map) ? map : $game_map + @viewport = viewport + @effect = [] + @disposed = false + @firsttime = true + @effects = { + # PinkMan's Effects + "fire" => Particle_Engine::Fire, + "smoke" => Particle_Engine::Smoke, + "teleport" => Particle_Engine::Teleport, + "spirit" => Particle_Engine::Spirit, + "explosion" => Particle_Engine::Explosion, + "aura" => Particle_Engine::Aura, + # BlueScope's Effects + "soot" => Particle_Engine::Soot, + "sootsmoke" => Particle_Engine::SootSmoke, + "rocket" => Particle_Engine::Rocket, + "fixteleport" => Particle_Engine::FixedTeleport, + "smokescreen" => Particle_Engine::Smokescreen, + "flare" => Particle_Engine::Flare, + "splash" => Particle_Engine::Splash, + # By Peter O. + "starteleport" => Particle_Engine::StarTeleport + } + end + + def dispose + return if disposed? + @effect.each do |particle| + next if particle.nil? + particle.dispose + end + @effect.clear + @map = nil + @disposed = true + end + + def disposed? + return @disposed + end + + def add_effect(event) + @effect[event.id] = pbParticleEffect(event) + end + + def remove_effect(event) + return if @effect[event.id].nil? + @effect[event.id].dispose + @effect.delete_at(event.id) + end + + def realloc_effect(event, particle) + type = pbEventCommentInput(event, 1, "Particle Engine Type") + if type.nil? + particle&.dispose + return nil + end + type = type[0].downcase + cls = @effects[type] + if cls.nil? + particle&.dispose + return nil + end + if !particle || !particle.is_a?(cls) + particle&.dispose + particle = cls.new(event, @viewport) + end + return particle + end + + def pbParticleEffect(event) + return realloc_effect(event, nil) + end + + def update + if @firsttime + @firsttime = false + @map.events.values.each do |event| + remove_effect(event) + add_effect(event) + end + end + @effect.each_with_index do |particle, i| + next if particle.nil? + if particle.event.pe_refresh + event = particle.event + event.pe_refresh = false + particle = realloc_effect(event, particle) + @effect[i] = particle + end + particle&.update + end + end +end + + + +class ParticleEffect + attr_accessor :x, :y, :z + + def initialize + @x = 0 + @y = 0 + @z = 0 + end + + def update; end + def dispose; end +end + + + +class ParticleSprite + attr_accessor :x, :y, :z, :ox, :oy, :opacity, :blend_type + attr_reader :bitmap + + def initialize(viewport) + @viewport = viewport + @sprite = nil + @x = 0 + @y = 0 + @z = 0 + @ox = 0 + @oy = 0 + @opacity = 255 + @bitmap = nil + @blend_type = 0 + @minleft = 0 + @mintop = 0 + end + + def dispose + @sprite&.dispose + end + + def bitmap=(value) + @bitmap = value + if value + @minleft = -value.width + @mintop = -value.height + else + @minleft = 0 + @mintop = 0 + end + end + + def update + w = Graphics.width + h = Graphics.height + if !@sprite && @x >= @minleft && @y >= @mintop && @x < w && @y < h + @sprite = Sprite.new(@viewport) + elsif @sprite && (@x < @minleft || @y < @mintop || @x >= w || @y >= h) + @sprite.dispose + @sprite = nil + end + if @sprite + @sprite.x = @x if @sprite.x != @x + @sprite.x -= @ox + @sprite.y = @y if @sprite.y != @y + @sprite.y -= @oy + @sprite.z = @z if @sprite.z != @z + @sprite.opacity = @opacity if @sprite.opacity != @opacity + @sprite.blend_type = @blend_type if @sprite.blend_type != @blend_type + @sprite.bitmap = @bitmap if @sprite.bitmap != @bitmap + end + end +end + + + +class ParticleEffect_Event < ParticleEffect + attr_accessor :event + + def initialize(event, viewport = nil) + @event = event + @viewport = viewport + @particles = [] + @bitmaps = {} + end + + def setParameters(params) + @randomhue, @leftright, @fade, + @maxparticless, @hue, @slowdown, + @ytop, @ybottom, @xleft, @xright, + @xgravity, @ygravity, @xoffset, @yoffset, + @opacityvar, @originalopacity = params + end + + def loadBitmap(filename, hue) + key = [filename, hue] + bitmap = @bitmaps[key] + if !bitmap || bitmap.disposed? + bitmap = AnimatedBitmap.new("Graphics/Fogs/" + filename, hue).deanimate + @bitmaps[key] = bitmap + end + return bitmap + end + + def initParticles(filename, opacity, zOffset = 0, blendtype = 1) + @particles = [] + @particlex = [] + @particley = [] + @opacity = [] + @startingx = self.x + @xoffset + @startingy = self.y + @yoffset + @screen_x = self.x + @screen_y = self.y + @real_x = @event.real_x + @real_y = @event.real_y + @filename = filename + @zoffset = zOffset + @bmwidth = 32 + @bmheight = 32 + @maxparticless.times do |i| + @particlex[i] = -@xoffset + @particley[i] = -@yoffset + @particles[i] = ParticleSprite.new(@viewport) + @particles[i].bitmap = loadBitmap(filename, @hue) if filename + if i == 0 && @particles[i].bitmap + @bmwidth = @particles[i].bitmap.width + @bmheight = @particles[i].bitmap.height + end + @particles[i].blend_type = blendtype + @particles[i].y = @startingy + @particles[i].x = @startingx + @particles[i].z = self.z + zOffset + @opacity[i] = rand(opacity / 4) + @particles[i].opacity = @opacity[i] + @particles[i].update + end + end + + def x; return ScreenPosHelper.pbScreenX(@event); end + def y; return ScreenPosHelper.pbScreenY(@event); end + def z; return ScreenPosHelper.pbScreenZ(@event); end + + def update + if @viewport && + (@viewport.rect.x >= Graphics.width || + @viewport.rect.y >= Graphics.height) + return + end + selfX = self.x + selfY = self.y + selfZ = self.z + newRealX = @event.real_x + newRealY = @event.real_y + @startingx = selfX + @xoffset + @startingy = selfY + @yoffset + @__offsetx = (@real_x == newRealX) ? 0 : selfX - @screen_x + @__offsety = (@real_y == newRealY) ? 0 : selfY - @screen_y + @screen_x = selfX + @screen_y = selfY + @real_x = newRealX + @real_y = newRealY + if @opacityvar > 0 && @viewport + opac = 255.0 / @opacityvar + minX = (opac * (-@xgravity.to_f / @slowdown).floor) + @startingx + maxX = (opac * (@xgravity.to_f / @slowdown).floor) + @startingx + minY = (opac * (-@ygravity.to_f / @slowdown).floor) + @startingy + maxY = @startingy + minX -= @bmwidth + minY -= @bmheight + maxX += @bmwidth + maxY += @bmheight + if maxX < 0 || maxY < 0 || minX >= Graphics.width || minY >= Graphics.height +# echo "skipped" + return + end + end + particleZ = selfZ + @zoffset + @maxparticless.times do |i| + @particles[i].z = particleZ + if @particles[i].y <= @ytop + @particles[i].y = @startingy + @yoffset + @particles[i].x = @startingx + @xoffset + @particlex[i] = 0.0 + @particley[i] = 0.0 + end + if @particles[i].x <= @xleft + @particles[i].y = @startingy + @yoffset + @particles[i].x = @startingx + @xoffset + @particlex[i] = 0.0 + @particley[i] = 0.0 + end + if @particles[i].y >= @ybottom + @particles[i].y = @startingy + @yoffset + @particles[i].x = @startingx + @xoffset + @particlex[i] = 0.0 + @particley[i] = 0.0 + end + if @particles[i].x >= @xright + @particles[i].y = @startingy + @yoffset + @particles[i].x = @startingx + @xoffset + @particlex[i] = 0.0 + @particley[i] = 0.0 + end + if @fade == 0 + if @opacity[i] <= 0 + @opacity[i] = @originalopacity + @particles[i].y = @startingy + @yoffset + @particles[i].x = @startingx + @xoffset + @particlex[i] = 0.0 + @particley[i] = 0.0 + end + elsif @opacity[i] <= 0 + @opacity[i] = 250 + @particles[i].y = @startingy + @yoffset + @particles[i].x = @startingx + @xoffset + @particlex[i] = 0.0 + @particley[i] = 0.0 + end + calcParticlePos(i) + if @randomhue == 1 + @hue += 0.5 + @hue = 0 if @hue >= 360 + @particles[i].bitmap = loadBitmap(@filename, @hue) if @filename + end + @opacity[i] = @opacity[i] - rand(@opacityvar) + @particles[i].opacity = @opacity[i] + @particles[i].update + end + end + + def calcParticlePos(i) + @leftright = rand(2) + if @leftright == 1 + xo = -@xgravity.to_f / @slowdown + else + xo = @xgravity.to_f / @slowdown + end + yo = -@ygravity.to_f / @slowdown + @particlex[i] += xo + @particley[i] += yo + @particlex[i] -= @__offsetx + @particley[i] -= @__offsety + @particlex[i] = @particlex[i].floor + @particley[i] = @particley[i].floor + @particles[i].x = @particlex[i] + @startingx + @xoffset + @particles[i].y = @particley[i] + @startingy + @yoffset + end + + def dispose + @particles.each do |particle| + particle.dispose + end + @bitmaps.values.each do |bitmap| + bitmap.dispose + end + @particles.clear + @bitmaps.clear + end +end + + + +class Particle_Engine::Fire < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 1, 20, 40, 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0.10, -5, -13, 30, 0]) + initParticles("particle", 250) + end +end + + + +class Particle_Engine::Smoke < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 0, 80, 20, 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0.10, -5, -15, 5, 80]) + initParticles("smoke", 250) + end +end + + + +class Particle_Engine::Teleport < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([1, 1, 1, 10, rand(360), 1, -64, + Graphics.height, -64, Graphics.width, 0, 3, -8, -15, 20, 0]) + initParticles("wideportal", 250) + @maxparticless.times do |i| + @particles[i].ox = 16 + @particles[i].oy = 16 + end + end +end + + + +class Particle_Engine::Spirit < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([1, 0, 1, 20, rand(360), 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0.10, -5, -13, 30, 0]) + initParticles("particle", 250) + end +end + + + +class Particle_Engine::Explosion < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 1, 20, 0, 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0.10, -5, -13, 30, 0]) + initParticles("explosion", 250) + end +end + + + +class Particle_Engine::Aura < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 1, 20, 0, 1, -64, + Graphics.height, -64, Graphics.width, 2, 2, -5, -13, 30, 0]) + initParticles("particle", 250) + end +end + + + +class Particle_Engine::Soot < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 0, 20, 0, 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0.10, -5, -15, 5, 80]) + initParticles("smoke", 100, 0, 2) + end +end + + + +class Particle_Engine::SootSmoke < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 0, 30, 0, 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0.10, -5, -15, 5, 80]) + initParticles("smoke", 100, 0) + @maxparticless.times do |i| + @particles[i].blend_type = rand(6) < 3 ? 1 : 2 + end + end +end + + + +class Particle_Engine::Rocket < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 0, 60, 0, 0.5, -64, + Graphics.height, -64, Graphics.width, 0.5, 0, -5, -15, 5, 80]) + initParticles("smoke", 100, -1) + end +end + + + +class Particle_Engine::FixedTeleport < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([1, 0, 1, 10, rand(360), 1, + -Graphics.height, Graphics.height, 0, Graphics.width, 0, 3, -8, -15, 20, 0]) + initParticles("wideportal", 250) + @maxparticless.times do |i| + @particles[i].ox = 16 + @particles[i].oy = 16 + end + end +end + + + +# By Peter O. +class Particle_Engine::StarTeleport < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 1, 10, 0, 1, + -Graphics.height, Graphics.height, 0, Graphics.width, 0, 3, -8, -15, 10, 0]) + initParticles("star", 250) + @maxparticless.times do |i| + @particles[i].ox = 48 + @particles[i].oy = 48 + end + end +end + + + +class Particle_Engine::Smokescreen < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 0, 250, 0, 0.2, -64, + Graphics.height, -64, Graphics.width, 0.8, 0.8, -5, -15, 5, 80]) + initParticles(nil, 100) + @maxparticless.times do |i| + rnd = rand(3) + @opacity[i] = (rnd == 0) ? 1 : 100 + filename = (rnd == 0) ? "explosionsmoke" : "smoke" + @particles[i].bitmap = loadBitmap(filename, @hue) + end + end + + def calcParticlePos(i) + if @randomhue == 1 + filename = (rand(3) == 0) ? "explosionsmoke" : "smoke" + @particles[i].bitmap = loadBitmap(filename, @hue) + end + multiple = 1.7 + xgrav = @xgravity * multiple / @slowdown + xgrav = -xgrav if rand(2) == 1 + ygrav = @ygravity * multiple / @slowdown + ygrav = -ygrav if rand(2) == 1 + @particlex[i] += xgrav + @particley[i] += ygrav + @particlex[i] -= @__offsetx + @particley[i] -= @__offsety + @particlex[i] = @particlex[i].floor + @particley[i] = @particley[i].floor + @particles[i].x = @particlex[i] + @startingx + @xoffset + @particles[i].y = @particley[i] + @startingy + @yoffset + end +end + + + +class Particle_Engine::Flare < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 1, 30, 10, 1, -64, + Graphics.height, -64, Graphics.width, 2, 2, -5, -12, 30, 0]) + initParticles("particle", 255) + end +end + + + +class Particle_Engine::Splash < ParticleEffect_Event + def initialize(event, viewport) + super + setParameters([0, 0, 1, 30, 255, 1, -64, + Graphics.height, -64, Graphics.width, 4, 2, -5, -12, 30, 0]) + initParticles("smoke", 50) + end + + def update + super + @maxparticless.times do |i| + @particles[i].opacity = 50 + @particles[i].update + end + end +end + + + +class Game_Event < Game_Character + attr_accessor :pe_refresh + + alias nf_particles_game_map_initialize initialize unless method_defined?(:nf_particles_game_map_initialize) + + def initialize(map_id, event, map = nil) + @pe_refresh = false + begin + nf_particles_game_map_initialize(map_id, event, map) + rescue ArgumentError + nf_particles_game_map_initialize(map_id, event) + end + end + + alias nf_particles_game_map_refresh refresh unless method_defined?(:nf_particles_game_map_refresh) + + def refresh + nf_particles_game_map_refresh + @pe_refresh = true + end +end diff --git a/Data/Scripts/005_Sprites/011_PictureEx.rb b/Data/Scripts/005_Sprites/011_PictureEx.rb new file mode 100644 index 000000000..404924d91 --- /dev/null +++ b/Data/Scripts/005_Sprites/011_PictureEx.rb @@ -0,0 +1,519 @@ +class PictureOrigin + TopLeft = 0 + Center = 1 + TopRight = 2 + BottomLeft = 3 + LowerLeft = 3 + BottomRight = 4 + LowerRight = 4 + Top = 5 + Bottom = 6 + Left = 7 + Right = 8 +end + + + +class Processes + XY = 0 + DeltaXY = 1 + Z = 2 + Curve = 3 + Zoom = 4 + Angle = 5 + Tone = 6 + Color = 7 + Hue = 8 + Opacity = 9 + Visible = 10 + BlendType = 11 + SE = 12 + Name = 13 + Origin = 14 + Src = 15 + SrcSize = 16 + CropBottom = 17 +end + + + +def getCubicPoint2(src,t) + x0 = src[0]; y0 = src[1] + cx0 = src[2]; cy0 = src[3] + cx1 = src[4]; cy1 = src[5] + x1 = src[6]; y1 = src[7] + + x1 = cx1+(x1-cx1)*t + x0 = x0+(cx0-x0)*t + cx0 = cx0+(cx1-cx0)*t + cx1 = cx0+(x1-cx0)*t + cx0 = x0+(cx0-x0)*t + cx = cx0+(cx1-cx0)*t + # a = x1 - 3 * cx1 + 3 * cx0 - x0 + # b = 3 * (cx1 - 2 * cx0 + x0) + # c = 3 * (cx0 - x0) + # d = x0 + # cx = a*t*t*t + b*t*t + c*t + d + y1 = cy1+(y1-cy1)*t + y0 = y0+(cy0-y0)*t + cy0 = cy0+(cy1-cy0)*t + cy1 = cy0+(y1-cy0)*t + cy0 = y0+(cy0-y0)*t + cy = cy0+(cy1-cy0)*t + # a = y1 - 3 * cy1 + 3 * cy0 - y0 + # b = 3 * (cy1 - 2 * cy0 + y0) + # c = 3 * (cy0 - y0) + # d = y0 + # cy = a*t*t*t + b*t*t + c*t + d + return [cx,cy] +end + + + +#=============================================================================== +# PictureEx +#=============================================================================== +class PictureEx + attr_accessor :x # x-coordinate + attr_accessor :y # y-coordinate + attr_accessor :z # z value + attr_accessor :zoom_x # x directional zoom rate + attr_accessor :zoom_y # y directional zoom rate + attr_accessor :angle # rotation angle + attr_accessor :tone # tone + attr_accessor :color # color + attr_accessor :hue # filename hue + attr_accessor :opacity # opacity level + attr_accessor :visible # visibility boolean + attr_accessor :blend_type # blend method + attr_accessor :name # file name + attr_accessor :origin # starting point + attr_reader :src_rect # source rect + attr_reader :cropBottom # crops sprite to above this y-coordinate + attr_reader :frameUpdates # Array of processes updated in a frame + + def initialize(z) + # process: [type, delay, total_duration, frame_counter, cb, etc.] + @processes = [] + @x = 0.0 + @y = 0.0 + @z = z + @zoom_x = 100.0 + @zoom_y = 100.0 + @angle = 0 + @rotate_speed = 0 + @tone = Tone.new(0, 0, 0, 0) + @tone_duration = 0 + @color = Color.new(0, 0, 0, 0) + @hue = 0 + @opacity = 255.0 + @visible = true + @blend_type = 0 + @name = "" + @origin = PictureOrigin::TopLeft + @src_rect = Rect.new(0,0,-1,-1) + @cropBottom = -1 + @frameUpdates = [] + end + + def callback(cb) + if cb.is_a?(Proc); cb.call(self) + elsif cb.is_a?(Array); cb[0].method(cb[1]).call(self) + elsif cb.is_a?(Method); cb.call(self) + end + end + + def setCallback(delay, cb=nil) + delay = ensureDelayAndDuration(delay) + @processes.push([nil,delay,0,0,cb]) + end + + def running? + return @processes.length>0 + end + + def totalDuration + ret = 0 + for process in @processes + dur = process[1]+process[2] + ret = dur if dur>ret + end + ret *= 20.0/Graphics.frame_rate + return ret.to_i + end + + def ensureDelayAndDuration(delay, duration=nil) + delay = self.totalDuration if delay<0 + delay *= Graphics.frame_rate/20.0 + if !duration.nil? + duration *= Graphics.frame_rate/20.0 + return delay.to_i, duration.to_i + end + return delay.to_i + end + + def ensureDelay(delay) + return ensureDelayAndDuration(delay) + end + + # speed is the angle to change by in 1/20 of a second. @rotate_speed is the + # angle to change by per frame. + # NOTE: This is not compatible with manually changing the angle at a certain + # point. If you make a sprite auto-rotate, you should not try to alter + # the angle another way too. + def rotate(speed) + @rotate_speed = speed*20.0/Graphics.frame_rate + while @rotate_speed<0; @rotate_speed += 360; end + @rotate_speed %= 360 + end + + def erase + self.name = "" + end + + def clearProcesses + @processes = [] + end + + def adjustPosition(xOffset, yOffset) + for process in @processes + next if process[0]!=Processes::XY + process[5] += xOffset + process[6] += yOffset + process[7] += xOffset + process[8] += yOffset + end + end + + def move(delay, duration, origin, x, y, zoom_x=100.0, zoom_y=100.0, opacity=255) + setOrigin(delay,duration,origin) + moveXY(delay,duration,x,y) + moveZoomXY(delay,duration,zoom_x,zoom_y) + moveOpacity(delay,duration,opacity) + end + + def moveXY(delay, duration, x, y, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::XY,delay,duration,0,cb,@x,@y,x,y]) + end + + def setXY(delay, x, y, cb=nil) + moveXY(delay,0,x,y,cb) + end + + def moveCurve(delay, duration, x1, y1, x2, y2, x3, y3, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::Curve,delay,duration,0,cb,[@x,@y,x1,y1,x2,y2,x3,y3]]) + end + + def moveDelta(delay, duration, x, y, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::DeltaXY,delay,duration,0,cb,@x,@y,x,y]) + end + + def setDelta(delay, x, y, cb=nil) + moveDelta(delay,0,x,y,cb) + end + + def moveZ(delay, duration, z, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::Z,delay,duration,0,cb,@z,z]) + end + + def setZ(delay, z, cb=nil) + moveZ(delay,0,z,cb) + end + + def moveZoomXY(delay, duration, zoom_x, zoom_y, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::Zoom,delay,duration,0,cb,@zoom_x,@zoom_y,zoom_x,zoom_y]) + end + + def setZoomXY(delay, zoom_x, zoom_y, cb=nil) + moveZoomXY(delay,0,zoom_x,zoom_y,cb) + end + + def moveZoom(delay, duration, zoom, cb=nil) + moveZoomXY(delay,duration,zoom,zoom,cb) + end + + def setZoom(delay, zoom, cb=nil) + moveZoomXY(delay,0,zoom,zoom,cb) + end + + def moveAngle(delay, duration, angle, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::Angle,delay,duration,0,cb,@angle,angle]) + end + + def setAngle(delay, angle, cb=nil) + moveAngle(delay,0,angle,cb) + end + + def moveTone(delay, duration, tone, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + target = (tone) ? tone.clone : Tone.new(0,0,0,0) + @processes.push([Processes::Tone,delay,duration,0,cb,@tone.clone,target]) + end + + def setTone(delay, tone, cb=nil) + moveTone(delay,0,tone,cb) + end + + def moveColor(delay, duration, color, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + target = (color) ? color.clone : Color.new(0,0,0,0) + @processes.push([Processes::Color,delay,duration,0,cb,@color.clone,target]) + end + + def setColor(delay, color, cb=nil) + moveColor(delay,0,color,cb) + end + + # Hue changes don't actually work. + def moveHue(delay, duration, hue, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::Hue,delay,duration,0,cb,@hue,hue]) + end + + # Hue changes don't actually work. + def setHue(delay, hue, cb=nil) + moveHue(delay,0,hue,cb) + end + + def moveOpacity(delay, duration, opacity, cb=nil) + delay, duration = ensureDelayAndDuration(delay,duration) + @processes.push([Processes::Opacity,delay,duration,0,cb,@opacity,opacity]) + end + + def setOpacity(delay, opacity, cb=nil) + moveOpacity(delay,0,opacity,cb) + end + + def setVisible(delay, visible, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::Visible,delay,0,0,cb,visible]) + end + + # Only values of 0 (normal), 1 (additive) and 2 (subtractive) are allowed. + def setBlendType(delay, blend, cb=nil) + delay = ensureDelayAndDuration(delay) + @processes.push([Processes::BlendType,delay,0,0,cb,blend]) + end + + def setSE(delay, seFile, volume=nil, pitch=nil, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::SE,delay,0,0,cb,seFile,volume,pitch]) + end + + def setName(delay, name, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::Name,delay,0,0,cb,name]) + end + + def setOrigin(delay, origin, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::Origin,delay,0,0,cb,origin]) + end + + def setSrc(delay, srcX, srcY, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::Src,delay,0,0,cb,srcX,srcY]) + end + + def setSrcSize(delay, srcWidth, srcHeight, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::SrcSize,delay,0,0,cb,srcWidth,srcHeight]) + end + + # Used to cut Pokémon sprites off when they faint and sink into the ground. + def setCropBottom(delay, y, cb=nil) + delay = ensureDelay(delay) + @processes.push([Processes::CropBottom,delay,0,0,cb,y]) + end + + def update + procEnded = false + @frameUpdates.clear + for i in 0...@processes.length + process = @processes[i] + # Decrease delay of processes that are scheduled to start later + if process[1]>=0 + # Set initial values if the process will start this frame + if process[1]==0 + case process[0] + when Processes::XY + process[5] = @x + process[6] = @y + when Processes::DeltaXY + process[5] = @x + process[6] = @y + process[7] += @x + process[8] += @y + when Processes::Curve + process[5][0] = @x + process[5][1] = @y + when Processes::Z + process[5] = @z + when Processes::Zoom + process[5] = @zoom_x + process[6] = @zoom_y + when Processes::Angle + process[5] = @angle + when Processes::Tone + process[5] = @tone.clone + when Processes::Color + process[5] = @color.clone + when Processes::Hue + process[5] = @hue + when Processes::Opacity + process[5] = @opacity + end + end + # Decrease delay counter + process[1] -= 1 + # Process hasn't started yet, skip to the next one + next if process[1]>=0 + end + # Update process + @frameUpdates.push(process[0]) if !@frameUpdates.include?(process[0]) + fra = (process[2]==0) ? 1 : process[3] # Frame counter + dur = (process[2]==0) ? 1 : process[2] # Total duration of process + case process[0] + when Processes::XY, Processes::DeltaXY + @x = process[5] + fra * (process[7] - process[5]) / dur + @y = process[6] + fra * (process[8] - process[6]) / dur + when Processes::Curve + @x, @y = getCubicPoint2(process[5],fra.to_f/dur) + when Processes::Z + @z = process[5] + fra * (process[6] - process[5]) / dur + when Processes::Zoom + @zoom_x = process[5] + fra * (process[7] - process[5]) / dur + @zoom_y = process[6] + fra * (process[8] - process[6]) / dur + when Processes::Angle + @angle = process[5] + fra * (process[6] - process[5]) / dur + when Processes::Tone + @tone.red = process[5].red + fra * (process[6].red - process[5].red) / dur + @tone.green = process[5].green + fra * (process[6].green - process[5].green) / dur + @tone.blue = process[5].blue + fra * (process[6].blue - process[5].blue) / dur + @tone.gray = process[5].gray + fra * (process[6].gray - process[5].gray) / dur + when Processes::Color + @color.red = process[5].red + fra * (process[6].red - process[5].red) / dur + @color.green = process[5].green + fra * (process[6].green - process[5].green) / dur + @color.blue = process[5].blue + fra * (process[6].blue - process[5].blue) / dur + @color.alpha = process[5].alpha + fra * (process[6].alpha - process[5].alpha) / dur + when Processes::Hue + @hue = (process[6] - process[5]).to_f / dur + when Processes::Opacity + @opacity = process[5] + fra * (process[6] - process[5]) / dur + when Processes::Visible + @visible = process[5] + when Processes::BlendType + @blend_type = process[5] + when Processes::SE + pbSEPlay(process[5],process[6],process[7]) + when Processes::Name + @name = process[5] + when Processes::Origin + @origin = process[5] + when Processes::Src + @src_rect.x = process[5] + @src_rect.y = process[6] + when Processes::SrcSize + @src_rect.width = process[5] + @src_rect.height = process[6] + when Processes::CropBottom + @cropBottom = process[5] + end + # Increase frame counter + process[3] += 1 + if process[3]>process[2] + # Process has ended, erase it + callback(process[4]) if process[4] + @processes[i] = nil + procEnded = true + end + end + # Clear out empty spaces in @processes array caused by finished processes + @processes.compact! if procEnded + # Add the constant rotation speed + if @rotate_speed != 0 + @frameUpdates.push(Processes::Angle) if !@frameUpdates.include?(Processes::Angle) + @angle += @rotate_speed + while @angle<0; @angle += 360; end + @angle %= 360 + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +def setPictureSprite(sprite, picture, iconSprite=false) + return if picture.frameUpdates.length==0 + for i in 0...picture.frameUpdates.length + case picture.frameUpdates[i] + when Processes::XY, Processes::DeltaXY + sprite.x = picture.x.round + sprite.y = picture.y.round + when Processes::Z + sprite.z = picture.z + when Processes::Zoom + sprite.zoom_x = picture.zoom_x / 100.0 + sprite.zoom_y = picture.zoom_y / 100.0 + when Processes::Angle + sprite.angle = picture.angle + when Processes::Tone + sprite.tone = picture.tone + when Processes::Color + sprite.color = picture.color + when Processes::Hue + # This doesn't do anything. + when Processes::BlendType + sprite.blend_type = picture.blend_type + when Processes::Opacity + sprite.opacity = picture.opacity + when Processes::Visible + sprite.visible = picture.visible + when Processes::Name + sprite.name = picture.name if iconSprite && sprite.name != picture.name + when Processes::Origin + case picture.origin + when PictureOrigin::TopLeft, PictureOrigin::Left, PictureOrigin::BottomLeft + sprite.ox = 0 + when PictureOrigin::Top, PictureOrigin::Center, PictureOrigin::Bottom + sprite.ox = (sprite.bitmap && !sprite.bitmap.disposed?) ? sprite.src_rect.width/2 : 0 + when PictureOrigin::TopRight, PictureOrigin::Right, PictureOrigin::BottomRight + sprite.ox = (sprite.bitmap && !sprite.bitmap.disposed?) ? sprite.src_rect.width : 0 + end + case picture.origin + when PictureOrigin::TopLeft, PictureOrigin::Top, PictureOrigin::TopRight + sprite.oy = 0 + when PictureOrigin::Left, PictureOrigin::Center, PictureOrigin::Right + sprite.oy = (sprite.bitmap && !sprite.bitmap.disposed?) ? sprite.src_rect.height/2 : 0 + when PictureOrigin::BottomLeft, PictureOrigin::Bottom, PictureOrigin::BottomRight + sprite.oy = (sprite.bitmap && !sprite.bitmap.disposed?) ? sprite.src_rect.height : 0 + end + when Processes::Src + next unless iconSprite && sprite.src_rect + sprite.src_rect.x = picture.src_rect.x + sprite.src_rect.y = picture.src_rect.y + when Processes::SrcSize + next unless iconSprite && sprite.src_rect + sprite.src_rect.width = picture.src_rect.width + sprite.src_rect.height = picture.src_rect.height + end + end + if iconSprite && sprite.src_rect && picture.cropBottom>=0 + spriteBottom = sprite.y-sprite.oy+sprite.src_rect.height + if spriteBottom>picture.cropBottom + sprite.src_rect.height = [picture.cropBottom-sprite.y+sprite.oy,0].max + end + end +end + +def setPictureIconSprite(sprite, picture) + setPictureSprite(sprite,picture,true) +end diff --git a/Data/Scripts/005_Sprites/012_Interpolators.rb b/Data/Scripts/005_Sprites/012_Interpolators.rb new file mode 100644 index 000000000..567defb50 --- /dev/null +++ b/Data/Scripts/005_Sprites/012_Interpolators.rb @@ -0,0 +1,172 @@ +class Interpolator + ZOOM_X = 1 + ZOOM_Y = 2 + X = 3 + Y = 4 + OPACITY = 5 + COLOR = 6 + WAIT = 7 + + def initialize + @tweening = false + @tweensteps = [] + @sprite = nil + @frames = 0 + @step = 0 + end + + def tweening? + return @tweening + end + + def tween(sprite,items,frames) + @tweensteps = [] + if sprite && !sprite.disposed? && frames>0 + @frames = frames + @step = 0 + @sprite = sprite + for item in items + case item[0] + when ZOOM_X + @tweensteps[item[0]] = [sprite.zoom_x,item[1]-sprite.zoom_x] + when ZOOM_Y + @tweensteps[item[0]] = [sprite.zoom_y,item[1]-sprite.zoom_y] + when X + @tweensteps[item[0]] = [sprite.x,item[1]-sprite.x] + when Y + @tweensteps[item[0]] = [sprite.y,item[1]-sprite.y] + when OPACITY + @tweensteps[item[0]] = [sprite.opacity,item[1]-sprite.opacity] + when COLOR + @tweensteps[item[0]] = [sprite.color.clone,Color.new( + item[1].red-sprite.color.red, + item[1].green-sprite.color.green, + item[1].blue-sprite.color.blue, + item[1].alpha-sprite.color.alpha + )] + end + end + @tweening = true + end + end + + def update + if @tweening + t = (@step*1.0)/@frames + for i in 0...@tweensteps.length + item = @tweensteps[i] + next if !item + case i + when ZOOM_X + @sprite.zoom_x = item[0]+item[1]*t + when ZOOM_Y + @sprite.zoom_y = item[0]+item[1]*t + when X + @sprite.x = item[0]+item[1]*t + when Y + @sprite.y = item[0]+item[1]*t + when OPACITY + @sprite.opacity = item[0]+item[1]*t + when COLOR + @sprite.color = Color.new( + item[0].red+item[1].red*t, + item[0].green+item[1].green*t, + item[0].blue+item[1].blue*t, + item[0].alpha+item[1].alpha*t + ) + end + end + @step += 1 + if @step==@frames + @step = 0 + @frames = 0 + @tweening = false + end + end + end +end + + + +class RectInterpolator + def initialize(oldrect,newrect,frames) + restart(oldrect,newrect,frames) + end + + def restart(oldrect,newrect,frames) + @oldrect = oldrect + @newrect = newrect + @frames = [frames,1].max + @curframe = 0 + @rect = oldrect.clone + end + + def set(rect) + rect.set(@rect.x,@rect.y,@rect.width,@rect.height) + end + + def done? + @curframe>@frames + end + + def update + return if done? + t = (@curframe*1.0/@frames) + x1 = @oldrect.x + x2 = @newrect.x + x = x1+t*(x2-x1) + y1 = @oldrect.y + y2 = @newrect.y + y = y1+t*(y2-y1) + rx1 = @oldrect.x+@oldrect.width + rx2 = @newrect.x+@newrect.width + rx = rx1+t*(rx2-rx1) + ry1 = @oldrect.y+@oldrect.height + ry2 = @newrect.y+@newrect.height + ry = ry1+t*(ry2-ry1) + minx = xrx ? x : rx + miny = yry ? y : ry + @rect.set(minx,miny,maxx-minx,maxy-miny) + @curframe += 1 + end +end + + + +class PointInterpolator + attr_reader :x + attr_reader :y + + def initialize(oldx,oldy,newx,newy,frames) + restart(oldx,oldy,newx,newy,frames) + end + + def restart(oldx,oldy,newx,newy,frames) + @oldx = oldx + @oldy = oldy + @newx = newx + @newy = newy + @frames = frames + @curframe = 0 + @x = oldx + @y = oldy + end + + def done? + @curframe>@frames + end + + def update + return if done? + t = (@curframe*1.0/@frames) + rx1 = @oldx + rx2 = @newx + @x = rx1+t*(rx2-rx1) + ry1 = @oldy + ry2 = @newy + @y = ry1+t*(ry2-ry1) + @curframe += 1 + end +end diff --git a/Data/Scripts/005_Sprites/013_ScreenPosHelper.rb b/Data/Scripts/005_Sprites/013_ScreenPosHelper.rb new file mode 100644 index 000000000..418bbd9bf --- /dev/null +++ b/Data/Scripts/005_Sprites/013_ScreenPosHelper.rb @@ -0,0 +1,43 @@ +module ScreenPosHelper + def self.pbScreenZoomX(ch) + return Game_Map::TILE_WIDTH/32.0 + end + + def self.pbScreenZoomY(ch) + return Game_Map::TILE_HEIGHT/32.0 + end + + def self.pbScreenX(ch) + return ch.screen_x + end + + def self.pbScreenY(ch) + return ch.screen_y + end + + @heightcache={} + + def self.bmHeight(bm) + h=@heightcache[bm] + if !h + bmap=AnimatedBitmap.new("Graphics/Characters/"+bm,0) + h=bmap.height + @heightcache[bm]=h + bmap.dispose + end + return h + end + + def self.pbScreenZ(ch,height=nil) + if height==nil + height=0 + if ch.tile_id > 0 + height=32 + elsif ch.character_name!="" + height=bmHeight(ch.character_name)/4 + end + end + ret=ch.screen_z(height) + return ret + end +end diff --git a/Data/Scripts/005_Sprites/013_Sprite_Player_Offsets.rb b/Data/Scripts/005_Sprites/013_Sprite_Player_Offsets.rb new file mode 100644 index 000000000..3618c71e5 --- /dev/null +++ b/Data/Scripts/005_Sprites/013_Sprite_Player_Offsets.rb @@ -0,0 +1,35 @@ + +#[FRAME1 [x,y]],[FRAME2 [x,y], etc.] +# +# exact number of pixels that the sprite needs to be moved for each frame +# add 2 pixels on even frames +module Outfit_Offsets + BASE_OFFSET = [[0, 0], [0, 0], [0, 0], [0, 0]] + + + RUN_OFFSETS_DOWN = [[0, 2], [0, 6], [0, 2], [0, 6]] + RUN_OFFSETS_LEFT = [[-2, -2], [-2, -2], [-2, -2], [-2, -2]] + RUN_OFFSETS_RIGHT = [[2, -2], [2, -2], [2, -2], [2, -2]] + RUN_OFFSETS_UP = [[0, -2], [0, -2], [0, -2], [0, -2]] + + SURF_OFFSETS_DOWN = [[0, -6], [0, -4], [0, -6], [0, -4]] + SURF_OFFSETS_LEFT = [[-2, -10], [-2, -8], [-2, -10], [-2, -8]] + SURF_OFFSETS_RIGHT = [[4, -10], [4, -8], [4, -10], [4, -8]] + #SURF_OFFSETS_UP = [[0, -6], [0, -4], [0, -6], [0, -4]] + SURF_OFFSETS_UP = [[0, -10], [0, -8], [0, -10], [0, -8]] + + DIVE_OFFSETS_DOWN = [[0, -6], [0, -4], [0, -6], [0, -4]] + DIVE_OFFSETS_LEFT = [[6, -8], [6, -6], [6, -8], [6, -6]] + DIVE_OFFSETS_RIGHT = [[-6, -8], [-6, -6], [-6, -8], [-6, -6]] + DIVE_OFFSETS_UP = [[0, -2], [0, 0], [0, -2], [0, 0]] + + BIKE_OFFSETS_DOWN = [[0, -2], [2, 0], [0, -2], [-2, 0]] + BIKE_OFFSETS_LEFT = [[-4, -4], [-2, -2], [-4, -4], [-6, -2]] + BIKE_OFFSETS_RIGHT = [[4, -4], [2, -2], [4, -4], [6, -2]] + BIKE_OFFSETS_UP = [[0, -2], [-2, 0], [0, -2], [2, 0]] + + FISH_OFFSETS_DOWN = [[0, -6], [0, -2], [0, -8], [2, -6]] + FISH_OFFSETS_LEFT = [[0, -8], [-6, -6], [0, -8], [2, -8]] + FISH_OFFSETS_RIGHT = [[0, -8], [6, -6], [0, -8], [-2, -8]] + FISH_OFFSETS_UP = [[0, -6], [0, -6], [0, -6], [2, -4]] +end diff --git a/Data/Scripts/005_Sprites/013_Sprite_Wearable.rb b/Data/Scripts/005_Sprites/013_Sprite_Wearable.rb new file mode 100644 index 000000000..dc8bf2173 --- /dev/null +++ b/Data/Scripts/005_Sprites/013_Sprite_Wearable.rb @@ -0,0 +1,170 @@ +class Sprite_Wearable < RPG::Sprite + attr_accessor :filename + attr_accessor :action + attr_accessor :sprite + + def initialize(player_sprite, filename, action, viewport, relative_z=0) + @player_sprite = player_sprite + @viewport = viewport + @sprite = Sprite.new(@viewport) + @wearableBitmap = AnimatedBitmap.new(filename) if pbResolveBitmap(filename) + @filename = filename + @sprite.bitmap = @wearableBitmap.bitmap if @wearableBitmap + @action = action + @color = 0 + @frameWidth = 80 #@sprite.width + @frameHeight = 80 #@sprite.height / 4 + @sprite.z = 0 + @relative_z=relative_z #relative to player + echoln(_INTL("init had at z = {1}, player sprite at {2}",@sprite.z,@player_sprite.z)) + + #Unused position offset + # @x_pos_base_offset = 0 + # @y_pos_base_offset = 0 + end + + def apply_sprite_offset(offsets_array, current_frame) + @sprite.x += offsets_array[current_frame][0] + @sprite.y += offsets_array[current_frame][1] + end + + def adjustPositionForScreenScrolling + return if !$game_map.scrolling? && !@was_just_scrolling + if $game_map.scrolling? + @was_just_scrolling=true + else + @was_just_scrolling=false + end + offset_x = 0 + offset_y = 0 + case $game_map.scroll_direction + when DIRECTION_RIGHT + offset_x=-8 + when DIRECTION_LEFT + offset_x=8 + when DIRECTION_UP + offset_y=8 + @sprite.z+=50 #weird layering glitch for some reason otherwise. It's reset to the correct value in the next animation frame + when DIRECTION_DOWN + offset_y=-8 + end + @sprite.x+=offset_x + @sprite.y+=offset_y + end + + + def set_sprite_position(action, direction, current_frame) + @sprite.x = @player_sprite.x - @player_sprite.ox + @sprite.y = @player_sprite.y - @player_sprite.oy + case action + when "run" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_DOWN, current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_LEFT, current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_RIGHT, current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_UP, current_frame) + end + when "surf" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::SURF_OFFSETS_DOWN,current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset( Outfit_Offsets::SURF_OFFSETS_LEFT,current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset( Outfit_Offsets::SURF_OFFSETS_RIGHT,current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset( Outfit_Offsets::SURF_OFFSETS_UP,current_frame) + end + when "dive" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::DIVE_OFFSETS_DOWN,current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset( Outfit_Offsets::DIVE_OFFSETS_LEFT,current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset( Outfit_Offsets::DIVE_OFFSETS_RIGHT,current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset( Outfit_Offsets::DIVE_OFFSETS_UP,current_frame) + end + when "bike" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::BIKE_OFFSETS_DOWN,current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset( Outfit_Offsets::BIKE_OFFSETS_LEFT,current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset( Outfit_Offsets::BIKE_OFFSETS_RIGHT,current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset( Outfit_Offsets::BIKE_OFFSETS_UP,current_frame) + end + when "fish" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::FISH_OFFSETS_DOWN,current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset( Outfit_Offsets::FISH_OFFSETS_LEFT,current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset( Outfit_Offsets::FISH_OFFSETS_RIGHT,current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset( Outfit_Offsets::FISH_OFFSETS_UP,current_frame) + end + else + @sprite.x = @player_sprite.x - @player_sprite.ox + @sprite.y = @player_sprite.y - @player_sprite.oy + end + adjustPositionForScreenScrolling() + + @sprite.y -= 2 if current_frame % 2 == 1 + end + + + def animate(action, frame=nil) + @action = action + current_frame = @player_sprite.character.pattern if !frame + direction = @player_sprite.character.direction + crop_spritesheet(direction) + adjust_layer() + set_sprite_position(@action, direction, current_frame) + end + + def update(action, filename,color) + @sprite.opacity = @player_sprite.opacity if @wearableBitmap + if filename != @filename || color != @color + if pbResolveBitmap(filename) + #echoln pbResolveBitmap(filename) + @wearableBitmap = AnimatedBitmap.new(filename,color) + @sprite.bitmap = @wearableBitmap.bitmap + else + @wearableBitmap = nil + @sprite.bitmap = nil + end + @color =color + @filename = filename + end + animate(action) + end + + def adjust_layer() + if @sprite.z != @player_sprite.z+@relative_z + @sprite.z = @player_sprite.z+@relative_z + end + end + + def crop_spritesheet(direction) + sprite_x = 0 + sprite_y = ((direction - 2) / 2) * @frameHeight + @sprite.src_rect.set(sprite_x, sprite_y, @frameWidth, @frameHeight) + end + + def dispose + return if @disposed + @sprite.dispose if @sprite + @sprite = nil + @disposed = true + end + + def disposed? + @disposed + end + + +end diff --git a/Data/Scripts/005_Sprites/014_Sprite_Hair.rb b/Data/Scripts/005_Sprites/014_Sprite_Hair.rb new file mode 100644 index 000000000..e020e7145 --- /dev/null +++ b/Data/Scripts/005_Sprites/014_Sprite_Hair.rb @@ -0,0 +1,85 @@ +class Sprite_Hair < Sprite_Wearable + def initialize(player_sprite, filename, action, viewport) + super + @relative_z = 1 + + #@sprite.z = @player_sprite.z + 1 + end + + def animate(action, frame = nil) + @action = action + current_frame = @player_sprite.character.pattern if !frame + direction = @player_sprite.character.direction + crop_spritesheet(direction, current_frame, action) + adjust_layer() + set_sprite_position(@action, direction, current_frame) + end + + def crop_spritesheet(direction, current_frame, action) + sprite_x = ((current_frame)) * @frameWidth + # Don't animate surf + sprite_x = 0 if action == "surf" + + sprite_y = ((direction - 2) / 2) * @frameHeight + @sprite.src_rect.set(sprite_x, sprite_y, @frameWidth, @frameHeight) + end + + def set_sprite_position(action, direction, current_frame) + @sprite.x = @player_sprite.x - @player_sprite.ox + @sprite.y = @player_sprite.y - @player_sprite.oy + case action + when "run" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_DOWN, current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_LEFT, current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_RIGHT, current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset(Outfit_Offsets::RUN_OFFSETS_UP, current_frame) + end + when "surf" + if direction == DIRECTION_DOWN # Always animate as if on the first frame + apply_sprite_offset(Outfit_Offsets::SURF_OFFSETS_DOWN, 0) + elsif direction == DIRECTION_LEFT + apply_sprite_offset(Outfit_Offsets::SURF_OFFSETS_LEFT, 0) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset(Outfit_Offsets::SURF_OFFSETS_RIGHT, 0) + elsif direction == DIRECTION_UP + apply_sprite_offset(Outfit_Offsets::SURF_OFFSETS_UP, 0) + end + when "dive" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::DIVE_OFFSETS_DOWN, current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset(Outfit_Offsets::DIVE_OFFSETS_LEFT, current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset(Outfit_Offsets::DIVE_OFFSETS_RIGHT, current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset(Outfit_Offsets::DIVE_OFFSETS_UP, current_frame) + end + when "bike" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::BIKE_OFFSETS_DOWN, current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset(Outfit_Offsets::BIKE_OFFSETS_LEFT, current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset(Outfit_Offsets::BIKE_OFFSETS_RIGHT, current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset(Outfit_Offsets::BIKE_OFFSETS_UP, current_frame) + end + when "fish" + if direction == DIRECTION_DOWN + apply_sprite_offset(Outfit_Offsets::FISH_OFFSETS_DOWN, current_frame) + elsif direction == DIRECTION_LEFT + apply_sprite_offset(Outfit_Offsets::FISH_OFFSETS_LEFT, current_frame) + elsif direction == DIRECTION_RIGHT + apply_sprite_offset(Outfit_Offsets::FISH_OFFSETS_RIGHT, current_frame) + elsif direction == DIRECTION_UP + apply_sprite_offset(Outfit_Offsets::FISH_OFFSETS_UP, current_frame) + end + end + adjustPositionForScreenScrolling() + end + +end \ No newline at end of file diff --git a/Data/Scripts/005_Sprites/014_Sprite_Hat.rb b/Data/Scripts/005_Sprites/014_Sprite_Hat.rb new file mode 100644 index 000000000..75191ab3d --- /dev/null +++ b/Data/Scripts/005_Sprites/014_Sprite_Hat.rb @@ -0,0 +1,8 @@ +class Sprite_Hat < Sprite_Wearable + def initialize(player_sprite, filename, action, viewport, relative_z=2) + super + @relative_z = relative_z + #@sprite.z = @player_sprite.z + 2 + + end +end \ No newline at end of file diff --git a/Data/Scripts/005_Sprites/016_Sprite_Player.rb b/Data/Scripts/005_Sprites/016_Sprite_Player.rb new file mode 100644 index 000000000..aa74f90b0 --- /dev/null +++ b/Data/Scripts/005_Sprites/016_Sprite_Player.rb @@ -0,0 +1,122 @@ +class Sprite_Player < Sprite_Character + def initialize(viewport, character = nil) + super + @viewport = viewport + @outfit_bitmap = nil + # @hat_bitmap = nil + # @hat2_bitmap = nil + + hatFilename = "" + hairFilename = "" + @hat = Sprite_Hat.new(self, hatFilename, @character_name, @viewport,3) + @hat2 = Sprite_Hat.new(self, hatFilename, @character_name, @viewport,2) + @hair = Sprite_Hair.new(self, hairFilename, @character_name, @viewport) + + @previous_skinTone = 0 + + @current_bitmap = nil + @previous_action =nil + echoln "init playa" + getClothedPlayerSprite(true) + end + + + def updateCharacterBitmap + skinTone = $Trainer.skin_tone ? $Trainer.skin_tone : 0 + baseBitmapFilename = getBaseOverworldSpriteFilename(@character_name, skinTone) + if !pbResolveBitmap(baseBitmapFilename) + baseBitmapFilename = Settings::PLAYER_GRAPHICS_FOLDER + @character_name + end + AnimatedBitmap.new(baseBitmapFilename, @character_hue) + end + + def applyDayNightTone + super + pbDayNightTint(@hat.sprite) if @hat && @hat.sprite.bitmap + pbDayNightTint(@hat2.sprite) if @hat2 && @hat2.sprite.bitmap + pbDayNightTint(@hair.sprite) if @hair && @hair.sprite.bitmap + end + + def opacity=(value) + super + @hat.sprite.opacity= value if @hat && @hat.sprite.bitmap + @hat2.sprite.opacity= value if @hat2 && @hat2.sprite.bitmap + @hair.sprite.opacity= value if @hair && @hair.sprite.bitmap + end + + def getClothedPlayerSprite(forceUpdate=false) + if @previous_action != @character_name || forceUpdate + @current_bitmap = generateClothedBitmap + end + @previous_action = @character_name + @hair.animate(@character_name) if @hair + @hat.animate(@character_name) if @hat + @hat2.animate(@character_name) if @hat2 + return @current_bitmap + end + + + def generateClothedBitmap() + @charbitmap.bitmap.clone #nekkid sprite + baseBitmap = @charbitmap.bitmap.clone #nekkid sprite + + outfitFilename = getOverworldOutfitFilename($Trainer.clothes, @character_name) # + outfitFilename = getOverworldOutfitFilename(Settings::PLAYER_TEMP_OUTFIT_FALLBACK) if !pbResolveBitmap(outfitFilename) + hairFilename = getOverworldHairFilename($Trainer.hair) + hatFilename = getOverworldHatFilename($Trainer.hat) + hat2Filename = getOverworldHatFilename($Trainer.hat2) + + hair_color_shift = $Trainer.hair_color + hat_color_shift = $Trainer.hat_color + hat2_color_shift = $Trainer.hat2_color + + clothes_color_shift = $Trainer.clothes_color + + hair_color_shift = 0 if !hair_color_shift + hat_color_shift = 0 if !hat_color_shift + hat2_color_shift = 0 if !hat2_color_shift + + clothes_color_shift = 0 if !clothes_color_shift + @hair.update(@character_name, hairFilename, hair_color_shift) if @hair + @hat.update(@character_name, hatFilename, hat_color_shift) if @hat + @hat2.update(@character_name, hat2Filename, hat2_color_shift) if @hat2 + + if !pbResolveBitmap(outfitFilename) + raise "No temp clothes graphics available" + end + + outfitBitmap = AnimatedBitmap.new(outfitFilename, clothes_color_shift) if pbResolveBitmap(outfitFilename) + baseBitmap.blt(0, 0, outfitBitmap.bitmap, outfitBitmap.bitmap.rect) if outfitBitmap + @previous_action = @character_name + return baseBitmap + end + + + + + def update + super + if Settings::GAME_ID == :IF_HOENN && $PokemonGlobal.diving + self.z = -4 + @hat.adjust_layer if @hat + @hat2.adjust_layer if @hat2 + @hair.adjust_layer if @hair + end + end + + def dispose + super + @hat.dispose if @hat + @hat2.dispose if @hat2 + @hair.dispose if @hair + end + + def pbLoadOutfitBitmap(outfitFileName) + begin + outfitBitmap = RPG::Cache.load_bitmap("", outfitFileName) + return outfitBitmap + rescue + return nil + end + end +end diff --git a/Data/Scripts/006_Map renderer/001_TilemapRenderer.rb b/Data/Scripts/006_Map renderer/001_TilemapRenderer.rb new file mode 100644 index 000000000..16fc8e224 --- /dev/null +++ b/Data/Scripts/006_Map renderer/001_TilemapRenderer.rb @@ -0,0 +1,763 @@ +#=============================================================================== +# +#=============================================================================== +class TilemapRenderer + attr_reader :tilesets + attr_reader :autotiles + attr_reader :custom_autotile_ids + attr_accessor :tone + attr_accessor :color + attr_reader :viewport + attr_accessor :ox # Does nothing + attr_accessor :oy # Does nothing + attr_accessor :visible # Does nothing + + DISPLAY_TILE_WIDTH = Game_Map::TILE_WIDTH rescue 32 + DISPLAY_TILE_HEIGHT = Game_Map::TILE_HEIGHT rescue 32 + SOURCE_TILE_WIDTH = 32 + SOURCE_TILE_HEIGHT = 32 + ZOOM_X = DISPLAY_TILE_WIDTH / SOURCE_TILE_WIDTH + ZOOM_Y = DISPLAY_TILE_HEIGHT / SOURCE_TILE_HEIGHT + TILESET_TILES_PER_ROW = 8 + AUTOTILES_COUNT = 8 # Counting the blank tile as an autotile + TILES_PER_AUTOTILE = 48 + TILESET_START_ID = AUTOTILES_COUNT * TILES_PER_AUTOTILE + # If an autotile's filename ends with "[x]", its frame duration will be x/20 + # seconds instead. + AUTOTILE_FRAME_DURATION = 5 # In 1/20ths of a second + + # Filenames of extra autotiles for each tileset. Each tileset's entry is an + # array containing two other arrays (you can leave either of those empty, but + # they must be defined): + # - The first sub-array is for large autotiles, i.e. ones with 48 different + # tile layouts. For example, "Brick path" and "Sea". + # - The second is for single tile autotiles. For example, "Flowers1" and + # "Waterfall" + # The top tiles of the tileset will instead use these autotiles. Large + # autotiles come first, in the same 8x6 layout as you see when you double- + # click on a real autotile in RMXP. After that are the single tile autotiles. + # Extra autotiles are only useful if the tiles are animated, because otherwise + # you just have some tiles which belong in the tileset instead. + + # Examples: + # 1 => [["Sand shore"], ["Flowers2"]], + # 2 => [[], ["Flowers2", "Waterfall", "Waterfall crest", "Waterfall bottom"]], + # 6 => [["Water rock", "Sea deep"], []] + + EXTRA_AUTOTILES = { + 1 => { #route-field + 996 => "flowers_orange[10]", + 991 => "flowers_pink[10]", + 999 => "flowers_yellow[10]", + 1007 => "flowers_blue[10]", + 1015 => "flowers_purple[10]", + 1023 => "flowers_red[10]", + 1031 => "flowers_grey[10]", + 1039 => "flowers_white[10]", + }, + 2 => { #small-town + 996 => "flowers_orange[10]", + 991 => "flowers_pink[10]", + 999 => "flowers_yellow[10]", + 1007 => "flowers_blue[10]", + 1015 => "flowers_purple[10]", + 1023 => "flowers_red[10]", + 1031 => "flowers_grey[10]", + 1039 => "flowers_white[10]", + + }, + + + 23 => { #outdoor + 1232 => "flowers_orange[10]", + 1240 => "flowers_pink[10]", + 1248 => "flowers_yellow[10]", + 1256 => "flowers_blue[10]", + 1264 => "flowers_purple[10]", + 1272 => "flowers_red[10]", + 1280 => "flowers_grey[10]", + 1288 => "flowers_white[10]", + + }, + 30 => { + 2620 => "flowers_orange[10]", + 2628 => "flowers_pink[10]", + 2636 => "flowers_yellow[10]", + 2644 => "flowers_blue[10]", + 2652 => "flowers_purple[10]", + 2660 => "flowers_red[10]", + 2668 => "flowers_grey[10]", + 2676 => "flowers_white[10]", + } + } + + WIND_TREE_AUTOTILES = { + 1 => { #Route-field + 864 => "tree_sway_single_1", + 865 => "tree_sway_single_2", + 872 => "tree_sway_single_3", + 873 => "tree_sway_single_4", + 880 => "tree_sway_single_5", + 881 => "tree_sway_single_6", + + + 866 => "tree_sway_group_1", + 867 => "tree_sway_group_2", + 874 => "tree_sway_group_3", + 875 => "tree_sway_group_4", + }, + + 2 => { #small-town + #trees + 864 => "tree_sway_single_1", + 865 => "tree_sway_single_2", + 872 => "tree_sway_single_3", + 873 => "tree_sway_single_4", + 880 => "tree_sway_single_5", + 881 => "tree_sway_single_6", + + + 866 => "tree_sway_group_1", + 867 => "tree_sway_group_2", + 874 => "tree_sway_group_3", + 875 => "tree_sway_group_4", + }, + + } + + #============================================================================= + # + #============================================================================= + class TilesetBitmaps + attr_accessor :changed + attr_accessor :bitmaps + + def initialize + @bitmaps = {} + @bitmap_wraps = {} # Whether each tileset is a mega texture and has multiple columns + @load_counts = {} + @bridge = 0 + @changed = true + end + + def [](filename) + return @bitmaps[filename] + end + + def []=(filename, bitmap) + return if nil_or_empty?(filename) + @bitmaps[filename] = bitmap + @bitmap_wraps[filename] = false + @changed = true + end + + def add(filename) + return if nil_or_empty?(filename) + if @bitmaps[filename] + @load_counts[filename] += 1 + return + end + bitmap = pbGetTileset(filename) + @bitmap_wraps[filename] = false + if bitmap.mega? + self[filename] = TilemapRenderer::TilesetWrapper.wrapTileset(bitmap) + @bitmap_wraps[filename] = true + bitmap.dispose + else + self[filename] = bitmap + end + @load_counts[filename] = 1 + end + + def remove(filename) + return if nil_or_empty?(filename) || !@bitmaps[filename] + if @load_counts[filename] > 1 + @load_counts[filename] -= 1 + return + end + @bitmaps[filename].dispose + @bitmaps.delete(filename) + @bitmap_wraps.delete(filename) + @load_counts.delete(filename) + end + + def set_src_rect(tile, tile_id) + return if nil_or_empty?(tile.filename) + return if !@bitmaps[tile.filename] + tile.src_rect.x = ((tile_id - TILESET_START_ID) % TILESET_TILES_PER_ROW) * SOURCE_TILE_WIDTH + tile.src_rect.y = ((tile_id - TILESET_START_ID) / TILESET_TILES_PER_ROW) * SOURCE_TILE_HEIGHT + if @bitmap_wraps[tile.filename] + height = @bitmaps[tile.filename].height + col = (tile_id - TILESET_START_ID) * SOURCE_TILE_HEIGHT / (TILESET_TILES_PER_ROW * height) + tile.src_rect.x += col * TILESET_TILES_PER_ROW * SOURCE_TILE_WIDTH + tile.src_rect.y -= col * height + end + end + + def update; end + end + + #============================================================================= + # + #============================================================================= + class AutotileBitmaps < TilesetBitmaps + attr_reader :current_frames + + def initialize + super + @frame_counts = {} # Number of frames in each autotile + @frame_durations = {} # How long each frame lasts per autotile + @current_frames = {} # Which frame each autotile is currently showing + @timer = 0.0 # System.uptime + end + + def []=(filename, value) + super + return if nil_or_empty?(filename) + frame_count(filename, true) + set_current_frame(filename) + end + + EXPANDED_AUTOTILES_FOLDER = "Graphics/Autotiles/ExpandedAutotiles/" + def add(filename) + return if nil_or_empty?(filename) + if @bitmaps[filename] + @load_counts[filename] += 1 + return + end + + # Try to load expanded autotile from cache first + cached_path = File.join("Graphics", "Autotiles/ExpandedAutotiles", "#{filename}.png") + if safeExists?(cached_path) + #echoln "Loading cached expanded autotile for #{filename}" + bitmap = RPG::Cache.load_bitmap(EXPANDED_AUTOTILES_FOLDER, filename) + + duration = AUTOTILE_FRAME_DURATION + if filename[/\[\s*(\d+?)\s*\]\s*$/] + duration = $~[1].to_i + end + @frame_durations[filename] = duration.to_f / 20 + + else + orig_bitmap = pbGetAutotile(filename) + @bitmap_wraps[filename] = false + duration = AUTOTILE_FRAME_DURATION + if filename[/\[\s*(\d+?)\s*\]\s*$/] + duration = $~[1].to_i + end + @frame_durations[filename] = duration.to_f / 20 + expanded_bitmap = AutotileExpander.expand(orig_bitmap) + + # Save expanded bitmap to cache for next time + Dir.mkdir(EXPANDED_AUTOTILES_FOLDER) unless Dir.exist?(EXPANDED_AUTOTILES_FOLDER) + expanded_bitmap.save_to_png(cached_path) + + bitmap = expanded_bitmap + orig_bitmap.dispose if orig_bitmap != expanded_bitmap + end + + self[filename] = bitmap + if bitmap.height > SOURCE_TILE_HEIGHT && bitmap.height < TILES_PER_AUTOTILE * SOURCE_TILE_HEIGHT + @bitmap_wraps[filename] = true + end + @load_counts[filename] = 1 + end + + + def remove(filename) + super + return if @load_counts[filename] && @load_counts[filename] > 0 + @frame_counts.delete(filename) + @current_frames.delete(filename) + @frame_durations.delete(filename) + end + + def frame_count(filename, force_recalc = false) + if !@frame_counts[filename] || force_recalc + return 0 if !@bitmaps[filename] + bitmap = @bitmaps[filename] + @frame_counts[filename] = [bitmap.width / SOURCE_TILE_WIDTH, 1].max + if bitmap.height > SOURCE_TILE_HEIGHT && @bitmap_wraps[filename] + @frame_counts[filename] /= 2 + end + end + return @frame_counts[filename] + end + + def animated?(filename) + return frame_count(filename) > 1 + end + + def current_frame(filename) + set_current_frame(filename) if !@current_frames[filename] + return @current_frames[filename] + end + + def set_current_frame(filename) + frames = frame_count(filename) + if frames < 2 + @current_frames[filename] = 0 + else + @current_frames[filename] = (@timer / @frame_durations[filename]).floor % frames + end + end + + def set_src_rect(tile, tile_id) + filename = tile.filename + + # Check if this tile_id was overridden to use a specific autotile + override_filename = @custom_autotile_ids && @custom_autotile_ids[tile_id] + filename = override_filename if override_filename + + return if nil_or_empty?(filename) + return unless @bitmaps[filename] + + frame = current_frame(filename) + + if @bitmaps[filename].height == SOURCE_TILE_HEIGHT + tile.src_rect.x = frame * SOURCE_TILE_WIDTH + tile.src_rect.y = 0 + return + end + + wraps = @bitmap_wraps[filename] + high_id = ((tile_id % TILES_PER_AUTOTILE) >= TILES_PER_AUTOTILE / 2) + tile.src_rect.x = 0 + tile.src_rect.y = (tile_id % TILES_PER_AUTOTILE) * SOURCE_TILE_HEIGHT + if wraps && high_id + tile.src_rect.x = SOURCE_TILE_WIDTH + tile.src_rect.y -= SOURCE_TILE_HEIGHT * TILES_PER_AUTOTILE / 2 + end + tile.src_rect.x += frame * SOURCE_TILE_WIDTH * (wraps ? 2 : 1) + + # Override the filename in the tile object for consistency + tile.filename = filename if override_filename + end + + def update + super + @timer += Graphics.delta_s + # Update the current frame for each autotile + @bitmaps.each_key do |filename| + next if !@bitmaps[filename] || @bitmaps[filename].disposed? + old_frame = @current_frames[filename] + set_current_frame(filename) + @changed = true if @current_frames[filename] != old_frame + end + end + end + + #============================================================================= + # + #============================================================================= + class TileSprite < Sprite + attr_accessor :filename + attr_accessor :tile_id + attr_accessor :is_autotile + attr_accessor :animated + attr_accessor :priority + attr_accessor :shows_reflection + attr_accessor :underwater_tile + attr_accessor :bridge + attr_accessor :need_refresh + + def set_bitmap(filename, tile_id, autotile, animated, priority, bitmap) + self.bitmap = bitmap + self.src_rect = Rect.new(0, 0, SOURCE_TILE_WIDTH, SOURCE_TILE_HEIGHT) + self.zoom_x = ZOOM_X + self.zoom_y = ZOOM_Y + @filename = filename + @tile_id = tile_id + @is_autotile = autotile + @animated = animated + @priority = priority + @shows_reflection = false + @bridge = false + self.visible = !bitmap.nil? + @need_refresh = true + end + end + + #----------------------------------------------------------------------------- + + def initialize(viewport) + @tilesets = TilesetBitmaps.new + @autotiles = AutotileBitmaps.new + @custom_autotile_ids = {} # key: tile_id, value: filename + @tiles_horizontal_count = (Graphics.width.to_f / DISPLAY_TILE_WIDTH).ceil + 1 + @tiles_vertical_count = (Graphics.height.to_f / DISPLAY_TILE_HEIGHT).ceil + 1 + @tone = Tone.new(0, 0, 0, 0) + @old_tone = Tone.new(0, 0, 0, 0) + @color = Color.new(0, 0, 0, 0) + @old_color = Color.new(0, 0, 0, 0) + @self_viewport = Viewport.new(0, 0, Graphics.width, Graphics.height) + @viewport = (viewport) ? viewport : @self_viewport + @old_viewport_ox = 0 + @old_viewport_oy = 0 + # NOTE: The extra tiles horizontally/vertically hang off the left and top + # edges of the screen, because the pixel_offset values are positive + # and are added to the tile sprite coordinates. + @tiles = [] + @tiles_horizontal_count.times do |i| + @tiles[i] = [] + @tiles_vertical_count.times do |j| + @tiles[i][j] = Array.new(3) { TileSprite.new(@viewport) } + end + end + @current_map_id = 0 + @tile_offset_x = 0 + @tile_offset_y = 0 + @pixel_offset_x = 0 + @pixel_offset_y = 0 + @ox = 0 + @oy = 0 + @visible = true + @need_refresh = true + @disposed = false + end + + def dispose + return if disposed? + @tiles.each do |col| + col.each do |coord| + coord.each { |tile| tile.dispose } + coord.clear + end + end + @tiles.clear + @tilesets.bitmaps.each_value { |bitmap| bitmap.dispose } + @tilesets.bitmaps.clear + @autotiles.bitmaps.each_value { |bitmap| bitmap.dispose } + @autotiles.bitmaps.clear + @self_viewport.dispose + @self_viewport = nil + @disposed = true + end + + def disposed? + return @disposed + end + + #----------------------------------------------------------------------------- + + def add_tileset(filename) + @tilesets.add(filename) + end + + def remove_tileset(filename) + @tilesets.remove(filename) + end + + def add_autotile(filename) + @autotiles.add(filename) + end + + def remove_autotile(filename) + @autotiles.remove(filename) + end + + def get_autotile_overrides(tileset_id,map_id) + base_overrides = EXTRA_AUTOTILES[tileset_id] || {} + return base_overrides unless $game_weather + wind_overrides =WIND_TREE_AUTOTILES[tileset_id] || {} + if $game_weather.map_current_weather_type(map_id) == :Wind && WIND_TREE_AUTOTILES[tileset_id] + return base_overrides.merge(wind_overrides) + end + return base_overrides + end + + def add_extra_autotiles(tileset_id,map_id) + overrides = get_autotile_overrides(tileset_id,map_id) + return if !overrides || overrides.empty? + overrides.each do |tile_id, filename| + @autotiles.add(filename) + @custom_autotile_ids[tile_id] = filename + end + end + + def remove_extra_autotiles(tileset_id) + return if !EXTRA_AUTOTILES[tileset_id] + EXTRA_AUTOTILES[tileset_id].each do |arr| + arr.each { |filename| remove_autotile(filename) } + end + end + + #----------------------------------------------------------------------------- + + def refresh + @need_refresh = true + end + + def refresh_tile_bitmap(tile, map, tile_id) + tile.tile_id = tile_id + if tile_id < TILES_PER_AUTOTILE + tile.set_bitmap("", tile_id, false, false, 0, nil) + tile.shows_reflection = false + tile.bridge = false + else + terrain_tag = map.terrain_tags[tile_id] || 0 + terrain_tag_data = GameData::TerrainTag.try_get(terrain_tag) + priority = map.priorities[tile_id] || 0 + single_autotile_start_id = TILESET_START_ID + true_tileset_start_id = TILESET_START_ID + # extra_autotile_arrays = EXTRA_AUTOTILES[map.tileset_id] + # if extra_autotile_arrays + # large_autotile_count = extra_autotile_arrays[0].length + # single_autotile_count = extra_autotile_arrays[1].length + # single_autotile_start_id += large_autotile_count * TILES_PER_AUTOTILE + # true_tileset_start_id += large_autotile_count * TILES_PER_AUTOTILE + # true_tileset_start_id += single_autotile_count + # end + + filename = nil + extra_autotile_hash = get_autotile_overrides(map.tileset_id,map.map_id) + + if extra_autotile_hash && extra_autotile_hash[tile_id] + # Custom tile_id override + filename = extra_autotile_hash[tile_id] + tile.set_bitmap(filename, tile_id, true, @autotiles.animated?(filename), + priority, @autotiles[filename]) + elsif tile_id < true_tileset_start_id + # Default behavior + if tile_id < TILESET_START_ID # Real autotiles + filename = map.autotile_names[(tile_id / TILES_PER_AUTOTILE) - 1] + elsif tile_id < single_autotile_start_id # Large extra autotiles + filename = extra_autotile_arrays[0][(tile_id - TILESET_START_ID) / TILES_PER_AUTOTILE] + else + # Single extra autotiles + filename = extra_autotile_arrays[1][tile_id - single_autotile_start_id] + end + tile.set_bitmap(filename, tile_id, true, @autotiles.animated?(filename), + priority, @autotiles[filename]) + else + filename = map.tileset_name + tile.set_bitmap(filename, tile_id, false, false, priority, @tilesets[filename]) + end + + tile.shows_reflection = terrain_tag_data&.shows_reflections + tile.underwater_tile = terrain_tag_data&.underwater + tile.bridge = terrain_tag_data&.bridge + end + refresh_tile_src_rect(tile, tile_id) + end + + def refresh_tile_src_rect(tile, tile_id) + if tile.is_autotile + @autotiles.set_src_rect(tile, tile_id) + else + @tilesets.set_src_rect(tile, tile_id) + end + end + + # For animated autotiles only + def refresh_tile_frame(tile, tile_id) + return if !tile.animated + @autotiles.set_src_rect(tile, tile_id) + end + + # x and y are the positions of tile within @tiles, not a map x/y + def refresh_tile_coordinates(tile, x, y) + tile.x = (x * DISPLAY_TILE_WIDTH) - @pixel_offset_x + tile.y = (y * DISPLAY_TILE_HEIGHT) - @pixel_offset_y + end + + def refresh_tile_z(tile, map, y, layer, tile_id) + if tile.underwater_tile#tile.shows_reflection -2000 + tile.z = -5 + elsif tile.bridge && $PokemonGlobal.bridge > 0 + tile.z = 0 + else + priority = tile.priority + tile.z = (priority == 0) ? 0 : y * DISPLAY_TILE_HEIGHT + priority * 32 + 32 + end + end + + def refresh_tile(tile, x, y, map, layer, tile_id) + refresh_tile_bitmap(tile, map, tile_id) + refresh_tile_coordinates(tile, x, y) + refresh_tile_z(tile, map, y, layer, tile_id) + tile.need_refresh = false + end + + #----------------------------------------------------------------------------- + + def check_if_screen_moved + ret = false + # Check for map change + if @current_map_id != $game_map.map_id + if MapFactoryHelper.hasConnections?(@current_map_id) + offsets = $MapFactory.getRelativePos(@current_map_id, 0, 0, $game_map.map_id, 0, 0) + if offsets + @tile_offset_x -= offsets[0] + @tile_offset_y -= offsets[1] + else + ret = true # Need a full refresh + end + else + ret = true + end + @current_map_id = $game_map.map_id + end + # Check for tile movement + current_map_display_x = ($game_map.display_x.to_f / Game_Map::X_SUBPIXELS).round + current_map_display_y = ($game_map.display_y.to_f / Game_Map::Y_SUBPIXELS).round + new_tile_offset_x = (current_map_display_x / SOURCE_TILE_WIDTH) * ZOOM_X + new_tile_offset_y = (current_map_display_y / SOURCE_TILE_HEIGHT) * ZOOM_Y + if new_tile_offset_x != @tile_offset_x + if new_tile_offset_x > @tile_offset_x + # Take tile stacks off the right and insert them at the beginning (left) + (new_tile_offset_x - @tile_offset_x).times do + c = @tiles.shift + @tiles.push(c) + c.each do |coord| + coord.each { |tile| tile.need_refresh = true } + end + end + else + # Take tile stacks off the beginning (left) and push them onto the end (right) + (@tile_offset_x - new_tile_offset_x).times do + c = @tiles.pop + @tiles.prepend(c) + c.each do |coord| + coord.each { |tile| tile.need_refresh = true } + end + end + end + @screen_moved = true + @tile_offset_x = new_tile_offset_x + end + if new_tile_offset_y != @tile_offset_y + if new_tile_offset_y > @tile_offset_y + # Take tile stacks off the bottom and insert them at the beginning (top) + @tiles.each do |col| + (new_tile_offset_y - @tile_offset_y).times do + c = col.shift + col.push(c) + c.each { |tile| tile.need_refresh = true } + end + end + else + # Take tile stacks off the beginning (top) and push them onto the end (bottom) + @tiles.each do |col| + (@tile_offset_y - new_tile_offset_y).times do + c = col.pop + col.prepend(c) + c.each { |tile| tile.need_refresh = true } + end + end + end + @screen_moved = true + @screen_moved_vertically = true + @tile_offset_y = new_tile_offset_y + end + # Check for pixel movement + new_pixel_offset_x = (current_map_display_x % SOURCE_TILE_WIDTH) * ZOOM_X + new_pixel_offset_y = (current_map_display_y % SOURCE_TILE_HEIGHT) * ZOOM_Y + if new_pixel_offset_x != @pixel_offset_x + @screen_moved = true + @pixel_offset_x = new_pixel_offset_x + end + if new_pixel_offset_y != @pixel_offset_y + @screen_moved = true + @screen_moved_vertically = true + @pixel_offset_y = new_pixel_offset_y + end + return ret + end + + #----------------------------------------------------------------------------- + + def update + # Update tone + if @old_tone != @tone + @tiles.each do |col| + col.each do |coord| + coord.each { |tile| tile.tone = @tone } + end + end + @old_tone = @tone.clone + end + # Update color + if @old_color != @color + @tiles.each do |col| + col.each do |coord| + coord.each { |tile| tile.color = @color } + end + end + @old_color = @color.clone + end + # Recalculate autotile frames + @tilesets.update + @autotiles.update + do_full_refresh = @need_refresh + if @viewport.ox != @old_viewport_ox || @viewport.oy != @old_viewport_oy + @old_viewport_ox = @viewport.ox + @old_viewport_oy = @viewport.oy + do_full_refresh = true + end + # Check whether the screen has moved since the last update + @screen_moved = false + @screen_moved_vertically = false + if $PokemonGlobal.bridge != @bridge + @bridge = $PokemonGlobal.bridge + @screen_moved_vertically = true # To update bridge tiles' z values + end + do_full_refresh = true if check_if_screen_moved + # Update all tile sprites + visited = [] + @tiles_horizontal_count.times do |i| + visited[i] = [] + @tiles_vertical_count.times { |j| visited[i][j] = false } + end + $MapFactory.maps.each do |map| + # Calculate x/y ranges of tile sprites that represent them + map_display_x = (map.display_x.to_f / Game_Map::X_SUBPIXELS).round + map_display_x = ((map_display_x + (Graphics.width / 2)) * ZOOM_X) - (Graphics.width / 2) if ZOOM_X != 1 + map_display_y = (map.display_y.to_f / Game_Map::Y_SUBPIXELS).round + map_display_y = ((map_display_y + (Graphics.height / 2)) * ZOOM_Y) - (Graphics.height / 2) if ZOOM_Y != 1 + map_display_x_tile = map_display_x / DISPLAY_TILE_WIDTH + map_display_y_tile = map_display_y / DISPLAY_TILE_HEIGHT + start_x = [-map_display_x_tile, 0].max + start_y = [-map_display_y_tile, 0].max + end_x = @tiles_horizontal_count - 1 + end_x = [end_x, map.width - map_display_x_tile - 1].min + end_y = @tiles_vertical_count - 1 + end_y = [end_y, map.height - map_display_y_tile - 1].min + next if start_x > end_x || start_y > end_y || end_x < 0 || end_y < 0 + # Update all tile sprites representing this map + (start_x..end_x).each do |i| + tile_x = i + map_display_x_tile + (start_y..end_y).each do |j| + tile_y = j + map_display_y_tile + @tiles[i][j].each_with_index do |tile, layer| + tile_id = map.data[tile_x, tile_y, layer] + if do_full_refresh || tile.need_refresh || tile.tile_id != tile_id + refresh_tile(tile, i, j, map, layer, tile_id) + else + refresh_tile_frame(tile, tile_id) if tile.animated && @autotiles.changed + # Update tile's x/y coordinates + refresh_tile_coordinates(tile, i, j) if @screen_moved + # Update tile's z value + refresh_tile_z(tile, map, j, layer, tile_id) if @screen_moved_vertically + end + end + # Record x/y as visited + visited[i][j] = true + end + end + end + # Clear all unvisited tile sprites + @tiles.each_with_index do |col, i| + col.each_with_index do |coord, j| + next if visited[i][j] + coord.each do |tile| + tile.set_bitmap("", 0, false, false, 0, nil) + tile.shows_reflection = false + tile.bridge = false + end + end + end + @need_refresh = false + @autotiles.changed = false + end +end diff --git a/Data/Scripts/006_Map renderer/002_TilesetWrapper.rb b/Data/Scripts/006_Map renderer/002_TilesetWrapper.rb new file mode 100644 index 000000000..8873ccfbc --- /dev/null +++ b/Data/Scripts/006_Map renderer/002_TilesetWrapper.rb @@ -0,0 +1,97 @@ +#=============================================================================== +# This module is a little fix that works around PC hardware limitations. Since +# Essentials isn't working with software rendering anymore, it now has to deal +# with the limits of the GPU. For the most part this is no big deal, but people +# do have some really big tilesets. +# +# The fix is simple enough: If your tileset is too big, a new bitmap will be +# constructed with all the excess pixels sent to the image's right side. This +# basically means that you now have a limit far higher than you should ever +# actually need. +# +# Hardware limit -> max tileset length: +# 1024px -> 4096px +# 2048px -> 16384px (enough to get the normal limit) +# 4096px -> 65536px (enough to load pretty much any tileset) +# 8192px -> 262144px +# 16384px -> 1048576px (what most people have at this point) +#=============================================================================== +class TilemapRenderer + module TilesetWrapper + TILESET_WIDTH = SOURCE_TILE_WIDTH * TILESET_TILES_PER_ROW + # Looks useless, but covers weird numbers given to mkxp.json or a funky driver + MAX_TEX_SIZE = (Bitmap.max_size / 1024) * 1024 + MAX_TEX_SIZE_BOOSTED = (MAX_TEX_SIZE**2) / TILESET_WIDTH + + module_function + + def wrapTileset(originalbmp) + width = originalbmp.width + height = originalbmp.height + if width == TILESET_WIDTH && originalbmp.mega? + columns = (height / MAX_TEX_SIZE.to_f).ceil + if columns * TILESET_WIDTH > MAX_TEX_SIZE + raise "Tileset is too long!\n\nSIZE: #{originalbmp.height}px\nHARDWARE LIMIT: #{MAX_TEX_SIZE}px\nBOOSTED LIMIT: #{MAX_TEX_SIZE_BOOSTED}px" + end + bmp = Bitmap.new(TILESET_WIDTH * columns, MAX_TEX_SIZE) + remainder = height % MAX_TEX_SIZE + remainder = MAX_TEX_SIZE if remainder == 0 + columns.times do |col| + srcrect = Rect.new(0, col * MAX_TEX_SIZE, width, (col + 1 == columns) ? remainder : MAX_TEX_SIZE) + bmp.blt(col * TILESET_WIDTH, 0, originalbmp, srcrect) + end + return bmp + end + return originalbmp + end + + def getWrappedRect(src_rect) + ret = Rect.new(0, 0, 0, 0) + col = (src_rect.y / MAX_TEX_SIZE.to_f).floor + ret.x = (col * TILESET_WIDTH) + src_rect.x.clamp(0, TILESET_WIDTH) + ret.y = src_rect.y % MAX_TEX_SIZE + ret.width = src_rect.width.clamp(0, TILESET_WIDTH - src_rect.x) + ret.height = src_rect.height.clamp(0, MAX_TEX_SIZE) + return ret + end + + #--------------------------------------------------------------------------- + + private + + def blitWrappedPixels(destX, destY, dest, src, srcrect) + if srcrect.y + srcrect.width < MAX_TEX_SIZE + # Save the processing power + dest.blt(destX, destY, src, srcrect) + return + end + merge = (srcrect.y % MAX_TEX_SIZE) > ((srcrect.y + srcrect.height) % MAX_TEX_SIZE) + srcrect_mod = getWrappedRect(srcrect) + if merge + # FIXME: won't work on heights longer than two columns, but nobody should need + # more than 32k pixels high at once anyway + side = { + :a => MAX_TEX_SIZE - srcrect_mod.y, + :b => srcrect_mod.height - MAX_TEX_SIZE + srcrect_mod.y + } + dest.blt(destX, destY, src, Rect.new(srcrect_mod.x, srcrect_mod.y, srcrect_mod.width, side[:a])) + dest.blt(destX, destY + side[:a], src, Rect.new(srcrect_mod.x + TILESET_WIDTH, 0, srcrect_mod.width, side[:b])) + else + dest.blt(destX, destY, src, srcrect_mod) + end + end + + def stretchBlitWrappedPixels(destrect, dest, src, srcrect) + if srcrect.y + srcrect.width < MAX_TEX_SIZE + # Save the processing power + dest.stretch_blt(destrect, src, srcrect) + return + end + # Does a regular blit to a non-megasurface, then stretch_blts that to + # the destination. Yes it is slow + tmp = Bitmap.new(srcrect.width, srcrect.height) + blitWrappedPixels(0, 0, tmp, src, srcrect) + dest.stretch_blt(destrect, tmp, Rect.new(0, 0, srcrect.width, srcrect.height)) + end + end +end diff --git a/Data/Scripts/006_Map renderer/003_AutotileExpander.rb b/Data/Scripts/006_Map renderer/003_AutotileExpander.rb new file mode 100644 index 000000000..2b0468a2b --- /dev/null +++ b/Data/Scripts/006_Map renderer/003_AutotileExpander.rb @@ -0,0 +1,75 @@ +#=============================================================================== +# +#=============================================================================== +class TilemapRenderer + module AutotileExpander + MAX_TEXTURE_SIZE = (Bitmap.max_size / 1024) * 1024 + + module_function + + # This doesn't allow for cache sizes smaller than 768, but if that applies + # to you, you've got bigger problems. + def expand(bitmap) + return bitmap if bitmap.height == SOURCE_TILE_HEIGHT + expanded_format = (bitmap.height == SOURCE_TILE_HEIGHT * 6) + wrap = false + if MAX_TEXTURE_SIZE < TILES_PER_AUTOTILE * SOURCE_TILE_HEIGHT + wrap = true # Each autotile will occupy two columns instead of one + end + frames_count = [bitmap.width / (3 * SOURCE_TILE_WIDTH), 1].max + new_bitmap = Bitmap.new(frames_count * (wrap ? 2 : 1) * SOURCE_TILE_WIDTH, + TILES_PER_AUTOTILE * SOURCE_TILE_HEIGHT / (wrap ? 2 : 1)) + rect = Rect.new(0, 0, SOURCE_TILE_WIDTH / 2, SOURCE_TILE_HEIGHT / 2) + TILES_PER_AUTOTILE.times do |id| + pattern = TileDrawingHelper::AUTOTILE_PATTERNS[id >> 3][id % TILESET_TILES_PER_ROW] + wrap_offset_x = (wrap && id >= TILES_PER_AUTOTILE / 2) ? SOURCE_TILE_WIDTH : 0 + wrap_offset_y = (wrap && id >= TILES_PER_AUTOTILE / 2) ? (TILES_PER_AUTOTILE / 2) * SOURCE_TILE_HEIGHT : 0 + frames_count.times do |frame| + if expanded_format && [1, 2, 4, 8].include?(id) + dest_x = frame * SOURCE_TILE_WIDTH * (wrap ? 2 : 1) + dest_x += wrap_offset_x + next if dest_x > MAX_TEXTURE_SIZE + dest_y = id * SOURCE_TILE_HEIGHT + dest_y -= wrap_offset_y + next if dest_y > MAX_TEXTURE_SIZE + case id + when 1 # Top left corner + new_bitmap.blt(dest_x, dest_y, bitmap, + Rect.new(frame * SOURCE_TILE_WIDTH * 3, SOURCE_TILE_HEIGHT * 4, + SOURCE_TILE_WIDTH, SOURCE_TILE_HEIGHT)) + when 2 # Top right corner + new_bitmap.blt(dest_x, dest_y, bitmap, + Rect.new(SOURCE_TILE_WIDTH + (frame * SOURCE_TILE_WIDTH * 3), SOURCE_TILE_HEIGHT * 4, + SOURCE_TILE_WIDTH, SOURCE_TILE_HEIGHT)) + when 4 # Bottom right corner + new_bitmap.blt(dest_x, dest_y, bitmap, + Rect.new(SOURCE_TILE_WIDTH + (frame * SOURCE_TILE_WIDTH * 3), SOURCE_TILE_HEIGHT * 5, + SOURCE_TILE_WIDTH, SOURCE_TILE_HEIGHT)) + when 8 # Bottom left corner + new_bitmap.blt(dest_x, dest_y, bitmap, + Rect.new(frame * SOURCE_TILE_WIDTH * 3, SOURCE_TILE_HEIGHT * 5, + SOURCE_TILE_WIDTH, SOURCE_TILE_HEIGHT)) + end + next + end + pattern.each_with_index do |src_chunk, i| + real_src_chunk = src_chunk - 1 + dest_x = (i % 2) * SOURCE_TILE_WIDTH / 2 + dest_x += frame * SOURCE_TILE_WIDTH * (wrap ? 2 : 1) + dest_x += wrap_offset_x + next if dest_x > MAX_TEXTURE_SIZE + dest_y = (i / 2) * SOURCE_TILE_HEIGHT / 2 + dest_y += id * SOURCE_TILE_HEIGHT + dest_y -= wrap_offset_y + next if dest_y > MAX_TEXTURE_SIZE + rect.x = (real_src_chunk % 6) * SOURCE_TILE_WIDTH / 2 + rect.x += SOURCE_TILE_WIDTH * 3 * frame + rect.y = (real_src_chunk / 6) * SOURCE_TILE_HEIGHT / 2 + new_bitmap.blt(dest_x, dest_y, bitmap, rect) + end + end + end + return new_bitmap + end + end +end diff --git a/Data/Scripts/006_Map renderer/004_TileDrawingHelper.rb b/Data/Scripts/006_Map renderer/004_TileDrawingHelper.rb new file mode 100644 index 000000000..a7e61c33a --- /dev/null +++ b/Data/Scripts/006_Map renderer/004_TileDrawingHelper.rb @@ -0,0 +1,246 @@ +#=============================================================================== +# +#=============================================================================== +class TileDrawingHelper + attr_accessor :tileset + attr_accessor :autotiles + + AUTOTILE_PATTERNS = [ + [[27, 28, 33, 34], [5, 28, 33, 34], [27, 6, 33, 34], [5, 6, 33, 34], + [27, 28, 33, 12], [5, 28, 33, 12], [27, 6, 33, 12], [5, 6, 33, 12]], + [[27, 28, 11, 34], [5, 28, 11, 34], [27, 6, 11, 34], [5, 6, 11, 34], + [27, 28, 11, 12], [5, 28, 11, 12], [27, 6, 11, 12], [5, 6, 11, 12]], + [[25, 26, 31, 32], [25, 6, 31, 32], [25, 26, 31, 12], [25, 6, 31, 12], + [15, 16, 21, 22], [15, 16, 21, 12], [15, 16, 11, 22], [15, 16, 11, 12]], + [[29, 30, 35, 36], [29, 30, 11, 36], [5, 30, 35, 36], [5, 30, 11, 36], + [39, 40, 45, 46], [5, 40, 45, 46], [39, 6, 45, 46], [5, 6, 45, 46]], + [[25, 30, 31, 36], [15, 16, 45, 46], [13, 14, 19, 20], [13, 14, 19, 12], + [17, 18, 23, 24], [17, 18, 11, 24], [41, 42, 47, 48], [5, 42, 47, 48]], + [[37, 38, 43, 44], [37, 6, 43, 44], [13, 18, 19, 24], [13, 14, 43, 44], + [37, 42, 43, 48], [17, 18, 47, 48], [13, 18, 43, 48], [1, 2, 7, 8]] + ] + + # converts neighbors returned from tableNeighbors to tile indexes + NEIGHBORS_TO_AUTOTILE_INDEX = [ + 46, 44, 46, 44, 43, 41, 43, 40, 46, 44, 46, 44, 43, 41, 43, 40, + 42, 32, 42, 32, 35, 19, 35, 18, 42, 32, 42, 32, 34, 17, 34, 16, + 46, 44, 46, 44, 43, 41, 43, 40, 46, 44, 46, 44, 43, 41, 43, 40, + 42, 32, 42, 32, 35, 19, 35, 18, 42, 32, 42, 32, 34, 17, 34, 16, + 45, 39, 45, 39, 33, 31, 33, 29, 45, 39, 45, 39, 33, 31, 33, 29, + 37, 27, 37, 27, 23, 15, 23, 13, 37, 27, 37, 27, 22, 11, 22, 9, + 45, 39, 45, 39, 33, 31, 33, 29, 45, 39, 45, 39, 33, 31, 33, 29, + 36, 26, 36, 26, 21, 7, 21, 5, 36, 26, 36, 26, 20, 3, 20, 1, + 46, 44, 46, 44, 43, 41, 43, 40, 46, 44, 46, 44, 43, 41, 43, 40, + 42, 32, 42, 32, 35, 19, 35, 18, 42, 32, 42, 32, 34, 17, 34, 16, + 46, 44, 46, 44, 43, 41, 43, 40, 46, 44, 46, 44, 43, 41, 43, 40, + 42, 32, 42, 32, 35, 19, 35, 18, 42, 32, 42, 32, 34, 17, 34, 16, + 45, 38, 45, 38, 33, 30, 33, 28, 45, 38, 45, 38, 33, 30, 33, 28, + 37, 25, 37, 25, 23, 14, 23, 12, 37, 25, 37, 25, 22, 10, 22, 8, + 45, 38, 45, 38, 33, 30, 33, 28, 45, 38, 45, 38, 33, 30, 33, 28, + 36, 24, 36, 24, 21, 6, 21, 4, 36, 24, 36, 24, 20, 2, 20, 0 + ] + + def self.tableNeighbors(data, x, y, layer = nil) + return 0 if x < 0 || x >= data.xsize + return 0 if y < 0 || y >= data.ysize + if layer.nil? + t = data[x, y] + else + t = data[x, y, layer] + end + xp1 = [x + 1, data.xsize - 1].min + yp1 = [y + 1, data.ysize - 1].min + xm1 = [x - 1, 0].max + ym1 = [y - 1, 0].max + i = 0 + if layer.nil? + i |= 0x01 if data[ x, ym1] == t # N + i |= 0x02 if data[xp1, ym1] == t # NE + i |= 0x04 if data[xp1, y] == t # E + i |= 0x08 if data[xp1, yp1] == t # SE + i |= 0x10 if data[ x, yp1] == t # S + i |= 0x20 if data[xm1, yp1] == t # SW + i |= 0x40 if data[xm1, y] == t # W + i |= 0x80 if data[xm1, ym1] == t # NW + else + i |= 0x01 if data[ x, ym1, layer] == t # N + i |= 0x02 if data[xp1, ym1, layer] == t # NE + i |= 0x04 if data[xp1, y, layer] == t # E + i |= 0x08 if data[xp1, yp1, layer] == t # SE + i |= 0x10 if data[ x, yp1, layer] == t # S + i |= 0x20 if data[xm1, yp1, layer] == t # SW + i |= 0x40 if data[xm1, y, layer] == t # W + i |= 0x80 if data[xm1, ym1, layer] == t # NW + end + return i + end + + def self.fromTileset(tileset) + bmtileset = pbGetTileset(tileset.tileset_name) + bmautotiles = [] + 7.times do |i| + bmautotiles.push(pbGetAutotile(tileset.autotile_names[i])) + end + return self.new(bmtileset, bmautotiles) + end + + #----------------------------------------------------------------------------- + + def initialize(tileset, autotiles) + if tileset.mega? + @tileset = TilemapRenderer::TilesetWrapper.wrapTileset(tileset) + tileset.dispose + @shouldWrap = true + else + @tileset = tileset + @shouldWrap = false + end + @autotiles = autotiles + end + + def dispose + @tileset&.dispose + @tileset = nil + @autotiles.each_with_index do |autotile, i| + autotile.dispose + @autotiles[i] = nil + end + end + + def bltSmallAutotile(bitmap, x, y, cxTile, cyTile, id, frame) + return if id >= 384 || frame < 0 || !@autotiles + autotile = @autotiles[(id / 48) - 1] + return if !autotile || autotile.disposed? + cxTile = [cxTile / 2, 1].max + cyTile = [cyTile / 2, 1].max + if autotile.height == 32 + anim = frame * 32 + src_rect = Rect.new(anim, 0, 32, 32) + bitmap.stretch_blt(Rect.new(x, y, cxTile * 2, cyTile * 2), autotile, src_rect) + else + anim = frame * 96 + id %= 48 + tiles = AUTOTILE_PATTERNS[id >> 3][id & 7] + src = Rect.new(0, 0, 0, 0) + 4.times do |i| + tile_position = tiles[i] - 1 + src.set(((tile_position % 6) * 16) + anim, (tile_position / 6) * 16, 16, 16) + bitmap.stretch_blt(Rect.new((i % 2 * cxTile) + x, (i / 2 * cyTile) + y, cxTile, cyTile), + autotile, src) + end + end + end + + def bltSmallRegularTile(bitmap, x, y, cxTile, cyTile, id) + return if id < 384 || !@tileset || @tileset.disposed? + rect = Rect.new(((id - 384) % 8) * 32, ((id - 384) / 8) * 32, 32, 32) + rect = TilemapRenderer::TilesetWrapper.getWrappedRect(rect) if @shouldWrap + bitmap.stretch_blt(Rect.new(x, y, cxTile, cyTile), @tileset, rect) + end + + def bltSmallTile(bitmap, x, y, cxTile, cyTile, id, frame = 0) + if id >= 384 + bltSmallRegularTile(bitmap, x, y, cxTile, cyTile, id) + elsif id > 0 + bltSmallAutotile(bitmap, x, y, cxTile, cyTile, id, frame) + end + end + + def bltAutotile(bitmap, x, y, id, frame) + bltSmallAutotile(bitmap, x, y, 32, 32, id, frame) + end + + def bltRegularTile(bitmap, x, y, id) + bltSmallRegularTile(bitmap, x, y, 32, 32, id) + end + + def bltTile(bitmap, x, y, id, frame = 0) + if id >= 384 + bltRegularTile(bitmap, x, y, id) + elsif id > 0 + bltAutotile(bitmap, x, y, id, frame) + end + end +end + +#=============================================================================== +# +#=============================================================================== +def createMinimap(mapid) + map = load_data(sprintf("Data/Map%03d.rxdata", mapid)) rescue nil + return Bitmap.new(32, 32) if !map + bitmap = Bitmap.new(map.width * 4, map.height * 4) + black=Color.new(0,0,0) + tilesets = $data_tilesets + tileset = tilesets[map.tileset_id] + return bitmap if !tileset + helper = TileDrawingHelper.fromTileset(tileset) + map.height.times do |y| + map.width.times do |x| + 3.times do |z| + id = map.data[x, y, z] + id = 0 if !id + helper.bltSmallTile(bitmap, x * 4, y * 4, 4, 4, id) + end + end + end + bitmap.fill_rect(0, 0, bitmap.width, 1, black) + bitmap.fill_rect(0, bitmap.height - 1, bitmap.width, 1, black) + bitmap.fill_rect(0, 0, 1, bitmap.height, black) + bitmap.fill_rect(bitmap.width - 1, 0, 1, bitmap.height, black) + return bitmap +end + +def bltMinimapAutotile(dstBitmap, x, y, srcBitmap, id) + return if id >= 48 || !srcBitmap || srcBitmap.disposed? + anim = 0 + cxTile = 3 + cyTile = 3 + tiles = TileDrawingHelper::AUTOTILE_PATTERNS[id >> 3][id & 7] + src = Rect.new(0, 0, 0, 0) + 4.times do |i| + tile_position = tiles[i] - 1 + src.set((tile_position % 6 * cxTile) + anim, + tile_position / 6 * cyTile, cxTile, cyTile) + dstBitmap.blt((i % 2 * cxTile) + x, (i / 2 * cyTile) + y, srcBitmap, src) + end +end + +def passable?(passages, tile_id) + return false if tile_id.nil? + passage = passages[tile_id] + return (passage && passage < 15) +end + +# Unused +def getPassabilityMinimap(mapid) + map = load_data(sprintf("Data/Map%03d.rxdata", mapid)) + tileset = $data_tilesets[map.tileset_id] + minimap = AnimatedBitmap.new("Graphics/UI/minimap_tiles") + ret = Bitmap.new(map.width * 6, map.height * 6) + passtable = Table.new(map.width, map.height) + passages = tileset.passages + map.width.times do |i| + map.height.times do |j| + pass = true + [2, 1, 0].each do |z| + if !passable?(passages, map.data[i, j, z]) + pass = false + break + end + end + passtable[i, j] = pass ? 1 : 0 + end + end + neighbors = TileDrawingHelper::NEIGHBORS_TO_AUTOTILE_INDEX + map.width.times do |i| + map.height.times do |j| + next if passtable[i, j] != 0 + nb = TileDrawingHelper.tableNeighbors(passtable, i, j) + tile = neighbors[nb] + bltMinimapAutotile(ret, i * 6, j * 6, minimap.bitmap, tile) + end + end + minimap.dispose + return ret +end diff --git a/Data/Scripts/007_Objects and windows/001_RPG_Cache.rb b/Data/Scripts/007_Objects and windows/001_RPG_Cache.rb new file mode 100644 index 000000000..9983699a4 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/001_RPG_Cache.rb @@ -0,0 +1,167 @@ +class Hangup < Exception; end + + + +module RPG + module Cache + def self.debug + t = Time.now + filename = t.strftime("%H %M %S.%L.txt") + File.open("cache_" + filename, "wb") { |f| + @cache.each do |key, value| + if !value + f.write("#{key} (nil)\r\n") + elsif value.disposed? + f.write("#{key} (disposed)\r\n") + else + f.write("#{key} (#{value.refcount}, #{value.width}x#{value.height})\r\n") + end + end + } + end + + def self.setKey(key, obj) + @cache[key] = obj + end + + def self.fromCache(i) + return nil if !@cache.include?(i) + obj = @cache[i] + return nil if obj && obj.disposed? + return obj + end + + def self.need_clearing() + return @cache.size >= 100 + end + + + def self.load_bitmap(folder_name, filename, hue = 0) + path = folder_name + filename + cached = true + ret = fromCache(path) + if !ret + if filename == "" + ret = BitmapWrapper.new(32, 32) + else + ret = BitmapWrapper.new(path) + end + @cache[path] = ret + cached = false + end + if hue == 0 + ret.addRef if cached + return ret + end + key = [path, hue] + ret2 = fromCache(key) + if ret2 + ret2.addRef + else + ret2 = ret.copy + ret2.hue_change(hue) + @cache[key] = ret2 + end + return ret2 + end + + def self.load_bitmap_path(path, hue = 0) + cached = true + ret = fromCache(path) + if !ret + if path == "" + ret = BitmapWrapper.new(32, 32) + else + ret = BitmapWrapper.new(path) + end + @cache[path] = ret + cached = false + end + if hue == 0 + ret.addRef if cached + return ret + end + key = [path, hue] + ret2 = fromCache(key) + if ret2 + ret2.addRef + else + ret2 = ret.copy + ret2.hue_change(hue) + @cache[key] = ret2 + end + return ret2 + end + + def self.tileEx(filename, tile_id, hue, width = 1, height = 1) + key = [filename, tile_id, hue, width, height] + ret = fromCache(key) + if ret + ret.addRef + else + ret = BitmapWrapper.new(32 * width, 32 * height) + x = (tile_id - 384) % 8 * 32 + y = (((tile_id - 384) / 8) - height + 1) * 32 + tileset = yield(filename) + ret.blt(0, 0, tileset, Rect.new(x, y, 32 * width, 32 * height)) + tileset.dispose + ret.hue_change(hue) if hue != 0 + @cache[key] = ret + end + return ret + end + + def self.tile(filename, tile_id, hue) + return self.tileEx(filename, tile_id, hue) { |f| self.tileset(f) } + end + + def self.transition(filename) + self.load_bitmap("Graphics/Transitions/", filename) + end + + def self.retain(folder_name, filename = "", hue = 0) + path = folder_name + filename + ret = fromCache(path) + if hue > 0 + key = [path, hue] + ret2 = fromCache(key) + if ret2 + ret2.never_dispose = true + return + end + end + ret.never_dispose = true if ret + end + end +end + + + +class BitmapWrapper < Bitmap + attr_reader :refcount + attr_accessor :never_dispose + def dispose + return if self.disposed? + @refcount -= 1 + super if @refcount <= 0 && !never_dispose + end + + def initialize(*arg) + super + @refcount = 1 + end + + def resetRef + @refcount = 1 + end + + def copy + bm = self.clone + bm.resetRef + return bm + end + + def addRef + @refcount += 1 + end +end diff --git a/Data/Scripts/007_Objects and windows/002_MessageConfig.rb b/Data/Scripts/007_Objects and windows/002_MessageConfig.rb new file mode 100644 index 000000000..7633a927d --- /dev/null +++ b/Data/Scripts/007_Objects and windows/002_MessageConfig.rb @@ -0,0 +1,818 @@ +module MessageConfig + LIGHT_TEXT_MAIN_COLOR = Color.new(248, 248, 248) + LIGHT_TEXT_SHADOW_COLOR = Color.new(72, 80, 88) + DARK_TEXT_MAIN_COLOR = Color.new(80, 80, 88) + DARK_TEXT_SHADOW_COLOR = Color.new(160, 160, 168) + + BLUE_TEXT_MAIN_COLOR = Color.new(35, 130, 200) + BLUE_TEXT_SHADOW_COLOR = Color.new(20, 75, 115) + + FONT_NAME = "Power Green" + FONT_SIZE = 29 + SMALL_FONT_NAME = "Power Green Small" + SMALL_FONT_SIZE = 25 + NARROW_FONT_NAME = "Power Green Narrow" + NARROW_FONT_SIZE = 29 + + BUBBLE_TEXT_BASE = Color.new(248,248,248)#(72,80,88)#DIALOG + BUBBLE_TEXT_SHADOW= Color.new(166,160,151) + + # 0 = Pause cursor is displayed at end of text + # 1 = Pause cursor is displayed at bottom right + # 2 = Pause cursor is displayed at lower middle side + CURSOR_POSITION = 1 + WINDOW_OPACITY = 255 + TEXT_SPEED = nil # can be positive to wait frames or negative to + # show multiple characters in a single frame + @@systemFrame = nil + @@defaultTextSkin = nil + @@textSpeed = nil + @@systemFont = nil + @@smallFont = nil + @@narrowFont = nil + + def self.pbDefaultSystemFrame + if $PokemonSystem + return pbResolveBitmap("Graphics/Windowskins/" + Settings::MENU_WINDOWSKINS[$PokemonSystem.frame]) || "" + else + return pbResolveBitmap("Graphics/Windowskins/" + Settings::MENU_WINDOWSKINS[0]) || "" + end + end + + def self.pbDefaultSpeechFrame + if $PokemonSystem + return pbResolveBitmap("Graphics/Windowskins/" + Settings::SPEECH_WINDOWSKINS[$PokemonSystem.textskin]) || "" + else + return pbResolveBitmap("Graphics/Windowskins/" + Settings::SPEECH_WINDOWSKINS[0]) || "" + end + end + + def self.pbDefaultWindowskin + skin=($data_system) ? $data_system.windowskin_name : nil + if skin && skin!="" + skin=pbResolveBitmap("Graphics/Windowskins/"+skin) || "" + end + skin=pbResolveBitmap("Graphics/System/Window") if nil_or_empty?(skin) + skin=pbResolveBitmap("Graphics/Windowskins/001-Blue01") if nil_or_empty?(skin) + return skin || "" + end + + def self.pbGetSystemFrame + if !@@systemFrame + skin=MessageConfig.pbDefaultSystemFrame + skin=MessageConfig.pbDefaultWindowskin if nil_or_empty?(skin) + @@systemFrame=skin || "" + end + return @@systemFrame + end + + def self.pbGetSpeechFrame + if !@@defaultTextSkin + skin=MessageConfig.pbDefaultSpeechFrame + skin=MessageConfig.pbDefaultWindowskin if nil_or_empty?(skin) + @@defaultTextSkin=skin || "" + end + return @@defaultTextSkin + end + + def self.pbSetSystemFrame(value) + @@systemFrame=pbResolveBitmap(value) || "" + end + + def self.pbSetSpeechFrame(value) + @@defaultTextSkin=pbResolveBitmap(value) || "" + end + + #----------------------------------------------------------------------------- + + def self.pbDefaultTextSpeed + return ($PokemonSystem) ? pbSettingToTextSpeed($PokemonSystem.textspeed) : pbSettingToTextSpeed(nil) + end + + def self.pbGetTextSpeed + @@textSpeed=pbDefaultTextSpeed if !@@textSpeed + return @@textSpeed + end + + def self.pbSetTextSpeed(value) + @@textSpeed=value + end + + def self.pbSettingToTextSpeed(speed) + case speed + when 0 then return 1 + when 1 then return -3 + when 2 then return -999 + end + return TEXT_SPEED || 1 + end + + #----------------------------------------------------------------------------- + + def self.pbDefaultSystemFontName + return MessageConfig.pbTryFonts(FONT_NAME) + end + + def self.pbDefaultSmallFontName + return MessageConfig.pbTryFonts(SMALL_FONT_NAME) + end + + def self.pbDefaultNarrowFontName + return MessageConfig.pbTryFonts(NARROW_FONT_NAME) + end + + def self.pbGetSystemFontName + @@systemFont = pbDefaultSystemFontName if !@@systemFont + return @@systemFont + end + + def self.pbGetSmallFontName + @@smallFont = pbDefaultSmallFontName if !@@smallFont + return @@smallFont + end + + def self.pbGetNarrowFontName + @@narrowFont = pbDefaultNarrowFontName if !@@narrowFont + return @@narrowFont + end + + def self.pbSetSystemFontName(value) + @@systemFont = MessageConfig.pbTryFonts(value) + @@systemFont = MessageConfig.pbDefaultSystemFontName if @@systemFont == "" + end + + def self.pbSetSmallFontName(value) + @@smallFont = MessageConfig.pbTryFonts(value) + @@smallFont = MessageConfig.pbDefaultSmallFontName if @@smallFont == "" + end + + def self.pbSetNarrowFontName(value) + @@narrowFont = MessageConfig.pbTryFonts(value) + @@narrowFont = MessageConfig.pbDefaultNarrowFontName if @@narrowFont == "" + end + + def self.pbTryFonts(*args) + for a in args + next if !a + if a.is_a?(String) + return a if Font.exist?(a) + elsif a.is_a?(Array) + for aa in a + ret = MessageConfig.pbTryFonts(aa) + return ret if ret != "" + end + end + end + return "" + end +end + + + +#=============================================================================== +# Position a window +#=============================================================================== +def pbBottomRight(window) + window.x=Graphics.width-window.width + window.y=Graphics.height-window.height +end + +def pbBottomLeft(window) + window.x=0 + window.y=Graphics.height-window.height +end + +def pbBottomLeftLines(window,lines,width=nil) + window.x=0 + window.width=width ? width : Graphics.width + window.height=(window.borderY rescue 32)+lines*32 + window.y=Graphics.height-window.height +end + +def pbPositionFaceWindow(facewindow,msgwindow) + return if !facewindow + if msgwindow + if facewindow.height<=msgwindow.height + facewindow.y=msgwindow.y + else + facewindow.y=msgwindow.y+msgwindow.height-facewindow.height + end + facewindow.x=Graphics.width-facewindow.width + msgwindow.x=0 + msgwindow.width=Graphics.width-facewindow.width + else + facewindow.height=Graphics.height if facewindow.height>Graphics.height + facewindow.x=0 + facewindow.y=0 + end +end + +def pbPositionNearMsgWindow(cmdwindow,msgwindow,side, x_offset=nil,y_offset=nil) + return if !cmdwindow + if msgwindow + height=[cmdwindow.height,Graphics.height-msgwindow.height].min + if cmdwindow.height!=height + cmdwindow.height=height + end + cmdwindow.y=msgwindow.y-cmdwindow.height + if cmdwindow.y<0 + cmdwindow.y=msgwindow.y+msgwindow.height + if cmdwindow.y+cmdwindow.height>Graphics.height + cmdwindow.y=msgwindow.y-cmdwindow.height + end + end + case side + when :left + cmdwindow.x=msgwindow.x + when :right + cmdwindow.x=msgwindow.x+msgwindow.width-cmdwindow.width + else + cmdwindow.x=msgwindow.x+msgwindow.width-cmdwindow.width + end + else + cmdwindow.height=Graphics.height if cmdwindow.height>Graphics.height + cmdwindow.x=0 + cmdwindow.y=0 + end + cmdwindow.x+= x_offset if x_offset + cmdwindow.y+= y_offset if y_offset + + +end + +# internal function +def pbRepositionMessageWindow(msgwindow, linecount=2) + msgwindow.height=32*linecount+msgwindow.borderY + msgwindow.y=(Graphics.height)-(msgwindow.height) + if $game_system && $game_system.respond_to?("message_position") + case $game_system.message_position + when 0 # up + msgwindow.y=0 + when 1 # middle + msgwindow.y=(Graphics.height/2)-(msgwindow.height/2) + when 2 + msgwindow.y=(Graphics.height)-(msgwindow.height) + end + end + if $game_system && $game_system.respond_to?("message_frame") + if $game_system.message_frame != 0 + msgwindow.opacity = 0 + end + end +end + +# internal function +def pbUpdateMsgWindowPos(msgwindow,event,eventChanged=false) + if event + if eventChanged + msgwindow.resizeToFit2(msgwindow.text,Graphics.width*2/3,msgwindow.height) + end + msgwindow.y=event.screen_y-48-msgwindow.height + if msgwindow.y<0 + msgwindow.y=event.screen_y+24 + end + msgwindow.x=event.screen_x-(msgwindow.width/2) + msgwindow.x=0 if msgwindow.x<0 + if msgwindow.x>Graphics.width-msgwindow.width + msgwindow.x=Graphics.width-msgwindow.width + end + else + curwidth=msgwindow.width + if curwidth!=Graphics.width + msgwindow.width=Graphics.width + msgwindow.width=Graphics.width + end + end +end + +#=============================================================================== +# Determine the colour of a background +#=============================================================================== +def isDarkBackground(background,rect=nil) + return true if !background || background.disposed? + rect = background.rect if !rect + return true if rect.width<=0 || rect.height<=0 + xSeg = (rect.width/16) + xLoop = (xSeg==0) ? 1 : 16 + xStart = (xSeg==0) ? rect.x+(rect.width/2) : rect.x+xSeg/2 + ySeg = (rect.height/16) + yLoop = (ySeg==0) ? 1 : 16 + yStart = (ySeg==0) ? rect.y+(rect.height/2) : rect.y+ySeg/2 + count = 0 + y = yStart + r = 0; g = 0; b = 0 + yLoop.times do + x = xStart + xLoop.times do + clr = background.get_pixel(x,y) + if clr.alpha!=0 + r += clr.red + g += clr.green + b += clr.blue + count += 1 + end + x += xSeg + end + y += ySeg + end + return true if count==0 + r /= count + g /= count + b /= count + return (r*0.299+g*0.587+b*0.114)<160 +end + +def isDarkWindowskin(windowskin) + if $PokemonTemp.speechbubble_bubble + return false if $PokemonTemp.speechbubble_bubble > 0 + end + return true if !windowskin || windowskin.disposed? + if windowskin.width==192 && windowskin.height==128 + return isDarkBackground(windowskin,Rect.new(0,0,128,128)) + elsif windowskin.width==128 && windowskin.height==128 + return isDarkBackground(windowskin,Rect.new(0,0,64,64)) + elsif windowskin.width==96 && windowskin.height==48 + return isDarkBackground(windowskin,Rect.new(32,16,16,16)) + else + clr = windowskin.get_pixel(windowskin.width/2, windowskin.height/2) + return (clr.red*0.299+clr.green*0.587+clr.blue*0.114)<160 + end +end + +#=============================================================================== +# Determine which text colours to use based on the darkness of the background +#=============================================================================== +def getSkinColor(windowskin,color,isDarkSkin) + if !windowskin || windowskin.disposed? || + windowskin.width!=128 || windowskin.height!=128 + # Base color, shadow color (these are reversed on dark windowskins) + textcolors = [ + "0070F8","78B8E8", # 1 Blue + "E82010","F8A8B8", # 2 Red + "60B048","B0D090", # 3 Green + "48D8D8","A8E0E0", # 4 Cyan + "D038B8","E8A0E0", # 5 Magenta + "E8D020","F8E888", # 6 Yellow + "A0A0A8","D0D0D8", # 7 Grey + "F0F0F8","C8C8D0", # 8 White + "9040E8","B8A8E0", # 9 Purple + "F89818","F8C898", # 10 Orange + colorToRgb32(MessageConfig::DARK_TEXT_MAIN_COLOR), + colorToRgb32(MessageConfig::DARK_TEXT_SHADOW_COLOR), # 11 Dark default + colorToRgb32(MessageConfig::LIGHT_TEXT_MAIN_COLOR), + colorToRgb32(MessageConfig::LIGHT_TEXT_SHADOW_COLOR) # 12 Light default + ] + if color==0 || color>textcolors.length/2 # No special colour, use default + if isDarkSkin # Dark background, light text + return shadowc3tag(MessageConfig::LIGHT_TEXT_MAIN_COLOR, MessageConfig::LIGHT_TEXT_SHADOW_COLOR) + end + # Light background, dark text + return shadowc3tag(MessageConfig::DARK_TEXT_MAIN_COLOR, MessageConfig::DARK_TEXT_SHADOW_COLOR) + end + # Special colour as listed above + if isDarkSkin && color!=12 # Dark background, light text + return sprintf("",textcolors[2*(color-1)+1],textcolors[2*(color-1)]) + end + # Light background, dark text + return sprintf("",textcolors[2*(color-1)],textcolors[2*(color-1)+1]) + else # VX windowskin + color = 0 if color>=32 + x = 64 + (color % 8) * 8 + y = 96 + (color / 8) * 8 + pixel = windowskin.get_pixel(x, y) + return shadowctagFromColor(pixel) + end +end + +def getDefaultTextColors(windowskin) + if !windowskin || windowskin.disposed? || + windowskin.width!=128 || windowskin.height!=128 + if isDarkWindowskin(windowskin) + return [MessageConfig::LIGHT_TEXT_MAIN_COLOR, MessageConfig::LIGHT_TEXT_SHADOW_COLOR] # White + else + return [MessageConfig::DARK_TEXT_MAIN_COLOR, MessageConfig::DARK_TEXT_SHADOW_COLOR] # Dark gray + end + else # VX windowskin + color = windowskin.get_pixel(64, 96) + shadow = nil + isDark = (color.red+color.green+color.blue)/3 < 128 + if isDark + shadow = Color.new(color.red+64,color.green+64,color.blue+64) + else + shadow = Color.new(color.red-64,color.green-64,color.blue-64) + end + return [color,shadow] + end +end + +#=============================================================================== +# Makes sure a bitmap exists +#=============================================================================== +def pbDoEnsureBitmap(bitmap,dwidth,dheight) + if !bitmap || bitmap.disposed? || bitmap.width0 : false +end + +# pbFadeOutIn(z) { block } +# Fades out the screen before a block is run and fades it back in after the +# block exits. z indicates the z-coordinate of the viewport used for this effect +def pbFadeOutIn(z=99999,nofadeout=false) + col=Color.new(0,0,0,0) + viewport=Viewport.new(0,0,Graphics.width,Graphics.height) + viewport.z=z + numFrames = (Graphics.frame_rate*Settings::FADEOUT_SPEED).floor + alphaDiff = (255.0/numFrames).ceil + for j in 0..numFrames + col.set(0,0,0,j*alphaDiff) + viewport.color=col + Graphics.update + Input.update + end + pbPushFade + begin + yield if block_given? + ensure + pbPopFade + if !nofadeout + for j in 0..numFrames + col.set(0,0,0,(numFrames-j)*alphaDiff) + viewport.color=col + Graphics.update + Input.update + end + end + viewport.dispose + end +end + +def pbFadeOutInWithUpdate(z,sprites,nofadeout=false) + col=Color.new(0,0,0,0) + viewport=Viewport.new(0,0,Graphics.width,Graphics.height) + viewport.z=z + numFrames = (Graphics.frame_rate*Settings::FADEOUT_SPEED).floor + alphaDiff = (255.0/numFrames).ceil + for j in 0..numFrames + col.set(0,0,0,j*alphaDiff) + viewport.color=col + pbUpdateSpriteHash(sprites) + Graphics.update + Input.update + end + pbPushFade + begin + yield if block_given? + ensure + pbPopFade + if !nofadeout + for j in 0..numFrames + col.set(0,0,0,(numFrames-j)*alphaDiff) + viewport.color=col + pbUpdateSpriteHash(sprites) + Graphics.update + Input.update + end + end + viewport.dispose + end +end + +# Similar to pbFadeOutIn, but pauses the music as it fades out. +# Requires scripts "Audio" (for bgm_pause) and "SpriteWindow" (for pbFadeOutIn). +def pbFadeOutInWithMusic(zViewport=99999) + playingBGS = $game_system.getPlayingBGS + playingBGM = $game_system.getPlayingBGM + $game_system.bgm_pause(1.0) + $game_system.bgs_pause(1.0) + pos = $game_system.bgm_position + pbFadeOutIn(zViewport) { + yield + $game_system.bgm_position = pos + $game_system.bgm_resume(playingBGM) + $game_system.bgs_resume(playingBGS) + } +end + +def pbFadeOutAndHide(sprites) + visiblesprites = {} + numFrames = (Graphics.frame_rate*Settings::FADEOUT_SPEED).floor + alphaDiff = (255.0/numFrames).ceil + pbDeactivateWindows(sprites) { + for j in 0..numFrames + pbSetSpritesToColor(sprites,Color.new(0,0,0,j*alphaDiff)) + (block_given?) ? yield : pbUpdateSpriteHash(sprites) + end + } + for i in sprites + next if !i[1] + next if pbDisposed?(i[1]) + visiblesprites[i[0]] = true if i[1].visible + i[1].visible = false + end + return visiblesprites +end + +def pbFadeInAndShow(sprites,visiblesprites=nil) + if visiblesprites + for i in visiblesprites + if i[1] && sprites[i[0]] && !pbDisposed?(sprites[i[0]]) + sprites[i[0]].visible = true + end + end + end + numFrames = (Graphics.frame_rate*Settings::FADEOUT_SPEED).floor + alphaDiff = (255.0/numFrames).ceil + pbDeactivateWindows(sprites) { + for j in 0..numFrames + pbSetSpritesToColor(sprites,Color.new(0,0,0,((numFrames-j)*alphaDiff))) + (block_given?) ? yield : pbUpdateSpriteHash(sprites) + end + } +end + +# Restores which windows are active for the given sprite hash. +# _activeStatuses_ is the result of a previous call to pbActivateWindows +def pbRestoreActivations(sprites,activeStatuses) + return if !sprites || !activeStatuses + for k in activeStatuses.keys + if sprites[k] && sprites[k].is_a?(Window) && !pbDisposed?(sprites[k]) + sprites[k].active=activeStatuses[k] ? true : false + end + end +end + +# Deactivates all windows. If a code block is given, deactivates all windows, +# runs the code in the block, and reactivates them. +def pbDeactivateWindows(sprites) + if block_given? + pbActivateWindow(sprites,nil) { yield } + else + pbActivateWindow(sprites,nil) + end +end + +# Activates a specific window of a sprite hash. _key_ is the key of the window +# in the sprite hash. If a code block is given, deactivates all windows except +# the specified window, runs the code in the block, and reactivates them. +def pbActivateWindow(sprites,key) + return if !sprites + activeStatuses={} + for i in sprites + if i[1] && i[1].is_a?(Window) && !pbDisposed?(i[1]) + activeStatuses[i[0]]=i[1].active + i[1].active=(i[0]==key) + end + end + if block_given? + begin + yield + ensure + pbRestoreActivations(sprites,activeStatuses) + end + return {} + else + return activeStatuses + end +end + +#=============================================================================== +# Create background planes for a sprite hash +#=============================================================================== +# Adds a background to the sprite hash. +# _planename_ is the hash key of the background. +# _background_ is a filename within the Graphics/Pictures/ folder and can be +# an animated image. +# _viewport_ is a viewport to place the background in. +def addBackgroundPlane(sprites,planename,background,viewport=nil) + sprites[planename]=AnimatedPlane.new(viewport) + bitmapName=pbResolveBitmap("Graphics/Pictures/#{background}") + if bitmapName==nil + # Plane should exist in any case + sprites[planename].bitmap=nil + sprites[planename].visible=false + else + sprites[planename].setBitmap(bitmapName) + for spr in sprites.values + if spr.is_a?(Window) + spr.windowskin=nil + end + end + end +end + +# Adds a background to the sprite hash. +# _planename_ is the hash key of the background. +# _background_ is a filename within the Graphics/Pictures/ folder and can be +# an animated image. +# _color_ is the color to use if the background can't be found. +# _viewport_ is a viewport to place the background in. +def addBackgroundOrColoredPlane(sprites,planename,background,color,viewport=nil) + bitmapName=pbResolveBitmap("Graphics/Pictures/#{background}") + if bitmapName==nil + # Plane should exist in any case + sprites[planename]=ColoredPlane.new(color,@viewport) + else + sprites[planename]=AnimatedPlane.new(viewport) + sprites[planename].setBitmap(bitmapName) + for spr in sprites.values + if spr.is_a?(Window) + spr.windowskin=nil + end + end + end +end + + + +#=============================================================================== +# Ensure required method definitions +#=============================================================================== +module Graphics + if !self.respond_to?("width") + def self.width; return 640; end + end + if !self.respond_to?("height") + def self.height; return 480; end + end +end + + + +if !defined?(_INTL) + def _INTL(*args) + string=args[0].clone + for i in 1...args.length + string.gsub!(/\{#{i}\}/,"#{args[i]}") + end + return string + end +end + +if !defined?(_ISPRINTF) + def _ISPRINTF(*args) + string=args[0].clone + for i in 1...args.length + string.gsub!(/\{#{i}\:([^\}]+?)\}/) { |m| + next sprintf("%"+$1,args[i]) + } + end + return string + end +end + +if !defined?(_MAPINTL) + def _MAPINTL(*args) + string=args[1].clone + for i in 2...args.length + string.gsub!(/\{#{i}\}/,"#{args[i+1]}") + end + return string + end +end diff --git a/Data/Scripts/007_Objects and windows/003_Window.rb b/Data/Scripts/007_Objects and windows/003_Window.rb new file mode 100644 index 000000000..0bd0a6f01 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/003_Window.rb @@ -0,0 +1,604 @@ +class WindowCursorRect < Rect + def initialize(window) + super(0, 0, 0, 0) + @window = window + end + + def empty + return unless needs_update?(0, 0, 0, 0) + + set(0, 0, 0, 0) + end + + def empty? + return self.x == 0 && self.y == 0 && self.width == 0 && self.height == 0 + end + + def set(x, y, width, height) + return unless needs_update?(x, y, width, height) + + super(x, y, width, height) + + @window.width = @window.width + end + + def height=(value) + super(value) + @window.width = @window.width + end + + def width=(value) + super(value) + @window.width = @window.width + end + + def x=(value) + super(value) + @window.width = @window.width + end + + def y=(value) + super(value) + @window.width = @window.width + end + + private + + def needs_update?(x, y, width, height) + return self.x != x || self.y != y || self.width != width || self.height != height + end +end + + +class Window + attr_reader :tone + attr_reader :color + attr_reader :blend_type + attr_reader :contents_blend_type + attr_reader :viewport + attr_reader :contents + attr_reader :ox + attr_reader :oy + attr_reader :x + attr_reader :y + attr_reader :z + attr_reader :width + attr_reader :active + attr_reader :pause + attr_reader :height + attr_reader :opacity + attr_reader :back_opacity + attr_reader :contents_opacity + attr_reader :visible + attr_reader :cursor_rect + attr_reader :openness + attr_reader :stretch + + def windowskin + @_windowskin + end + + def initialize(viewport=nil) + @sprites={} + @spritekeys=[ + "back", + "corner0","side0","scroll0", + "corner1","side1","scroll1", + "corner2","side2","scroll2", + "corner3","side3","scroll3", + "cursor","contents","pause" + ] + @sidebitmaps=[nil,nil,nil,nil] + @cursorbitmap=nil + @bgbitmap=nil + @viewport=viewport + for i in @spritekeys + @sprites[i]=Sprite.new(@viewport) + end + @disposed=false + @tone=Tone.new(0,0,0) + @color=Color.new(0,0,0,0) + @blankcontents=Bitmap.new(1,1) # RGSS2 requires this + @contents=@blankcontents + @_windowskin=nil + @rpgvx=false # Set to true to emulate RPGVX windows + @x=0 + @y=0 + @width=0 + @openness=255 + @height=0 + @ox=0 + @oy=0 + @z=0 + @stretch=true + @visible=true + @active=true + @blend_type=0 + @contents_blend_type=0 + @opacity=255 + @back_opacity=255 + @contents_opacity=255 + @cursor_rect=WindowCursorRect.new(self) + @cursorblink=0 + @cursoropacity=255 + @pause=false + @pauseopacity=255 + @pauseframe=0 + privRefresh(true) + end + + def dispose + if !self.disposed? + for i in @sprites + i[1].dispose if i[1] + @sprites[i[0]]=nil + end + for i in 0...@sidebitmaps.length + @sidebitmaps[i].dispose if @sidebitmaps[i] + @sidebitmaps[i]=nil + end + @blankcontents.dispose + @cursorbitmap.dispose if @cursorbitmap + @backbitmap.dispose if @backbitmap + @sprites.clear + @sidebitmaps.clear + @_windowskin=nil + @_contents=nil + @disposed=true + end + end + + def openness=(value) + @openness=value + @openness=0 if @openness<0 + @openness=255 if @openness>255 + privRefresh + end + + def stretch=(value) + @stretch=value + privRefresh(true) + end + + def visible=(value) + @visible=value + privRefresh + end + + def viewport=(value) + @viewport=value + for i in @spritekeys + @sprites[i].dispose + if @sprites[i].is_a?(Sprite) + @sprites[i]=Sprite.new(@viewport) + else + @sprites[i]=nil + end + end + privRefresh(true) + end + + def z=(value) + @z=value + privRefresh + end + + def disposed? + return @disposed + end + + def contents=(value) + @contents=value + privRefresh + end + + def windowskin=(value) + @_windowskin=value + if value && value.is_a?(Bitmap) && !value.disposed? && value.width==128 + @rpgvx=true + else + @rpgvx=false + end + privRefresh(true) + end + + def ox=(value) + @ox=value + privRefresh + end + + def active=(value) + @active=value + privRefresh(true) + end + + def cursor_rect=(value) + if !value + @cursor_rect.empty + else + @cursor_rect.set(value.x,value.y,value.width,value.height) + end + end + + def oy=(value) + @oy=value + privRefresh + end + + def width=(value) + @width=value + privRefresh(true) + end + + def height=(value) + @height=value + privRefresh(true) + end + + def pause=(value) + @pause=value + @pauseopacity=0 if !value + privRefresh + end + + def x=(value) + @x=value + privRefresh + end + + def y=(value) + @y=value + privRefresh + end + + def opacity=(value) + @opacity=value + @opacity=0 if @opacity<0 + @opacity=255 if @opacity>255 + privRefresh + end + + def back_opacity=(value) + @back_opacity=value + @back_opacity=0 if @back_opacity<0 + @back_opacity=255 if @back_opacity>255 + privRefresh + end + + def contents_opacity=(value) + @contents_opacity=value + @contents_opacity=0 if @contents_opacity<0 + @contents_opacity=255 if @contents_opacity>255 + privRefresh + end + + def tone=(value) + @tone=value + privRefresh + end + + def color=(value) + @color=value + privRefresh + end + + def blend_type=(value) + @blend_type=value + privRefresh + end + + def flash(color,duration) + return if disposed? + for i in @sprites + i[1].flash(color,duration) + end + end + + def update + return if disposed? + mustchange=false + if @active + if @cursorblink==0 + @cursoropacity-=8 + @cursorblink=1 if @cursoropacity<=128 + else + @cursoropacity+=8 + @cursorblink=0 if @cursoropacity>=255 + end + mustchange=true if !@cursor_rect.empty? + else + mustchange=true if @cursoropacity!=128 + @cursoropacity=128 + end + if @pause + @pauseframe=(Graphics.frame_count / 8) % 4 + @pauseopacity=[@pauseopacity+64,255].min + mustchange=true + end + privRefresh if mustchange + for i in @sprites + i[1].update + end + end + + private + + def ensureBitmap(bitmap,dwidth,dheight) + if !bitmap||bitmap.disposed?||bitmap.width 0 + @sprites["scroll1"].visible = @visible && hascontents && @ox > 0 + @sprites["scroll2"].visible = @visible && hascontents && + (@contents.width - @ox) > @width-32 + @sprites["scroll3"].visible = @visible && hascontents && + (@contents.height - @oy) > @height-32 + else + for i in 0...4 + @sprites["corner#{i}"].visible=false + @sprites["side#{i}"].visible=false + @sprites["scroll#{i}"].visible=false + end + @sprites["contents"].visible=@visible && @openness==255 + @sprites["contents"].color=@color + @sprites["contents"].tone=@tone + @sprites["contents"].blend_type=@contents_blend_type + @sprites["contents"].opacity=contopac + @sprites["back"].visible=false + @sprites["pause"].visible=false + @sprites["cursor"].visible=false + end + for i in @sprites + i[1].z=@z + end + if @rpgvx + @sprites["cursor"].z=@z # For Compatibility + @sprites["contents"].z=@z # For Compatibility + @sprites["pause"].z=@z # For Compatibility + else + @sprites["cursor"].z=@z+1 # For Compatibility + @sprites["contents"].z=@z+2 # For Compatibility + @sprites["pause"].z=@z+2 # For Compatibility + end + if @rpgvx + trimX=64 + trimY=0 + backRect=Rect.new(0,0,64,64) + blindsRect=Rect.new(0,64,64,64) + else + trimX=128 + trimY=0 + backRect=Rect.new(0,0,128,128) + blindsRect=nil + end + @sprites["corner0"].src_rect.set(trimX,trimY+0,16,16); + @sprites["corner1"].src_rect.set(trimX+48,trimY+0,16,16); + @sprites["corner2"].src_rect.set(trimX,trimY+48,16,16); + @sprites["corner3"].src_rect.set(trimX+48,trimY+48,16,16); + @sprites["scroll0"].src_rect.set(trimX+24, trimY+16, 16, 8) # up + @sprites["scroll3"].src_rect.set(trimX+24, trimY+40, 16, 8) # down + @sprites["scroll1"].src_rect.set(trimX+16, trimY+24, 8, 16) # left + @sprites["scroll2"].src_rect.set(trimX+40, trimY+24, 8, 16) # right + cursorX=trimX + cursorY=trimY+64 + sideRects=[ + Rect.new(trimX+16,trimY+0,32,16), + Rect.new(trimX,trimY+16,16,32), + Rect.new(trimX+48,trimY+16,16,32), + Rect.new(trimX+16,trimY+48,32,16) + ] + if @width>32 && @height>32 + @sprites["contents"].src_rect.set(@ox,@oy,@width-32,@height-32) + else + @sprites["contents"].src_rect.set(0,0,0,0) + end + pauseRects=[ + trimX+32,trimY+64, + trimX+48,trimY+64, + trimX+32,trimY+80, + trimX+48,trimY+80, + ] + pauseWidth=16 + pauseHeight=16 + @sprites["pause"].src_rect.set( + pauseRects[@pauseframe*2], + pauseRects[@pauseframe*2+1], + pauseWidth,pauseHeight + ) + @sprites["pause"].x=@x+(@width/2)-(pauseWidth/2) + @sprites["pause"].y=@y+@height-16 # 16 refers to skin margin + @sprites["contents"].x=@x+16 + @sprites["contents"].y=@y+16 + @sprites["corner0"].x=@x + @sprites["corner0"].y=@y + @sprites["corner1"].x=@x+@width-16 + @sprites["corner1"].y=@y + @sprites["corner2"].x=@x + @sprites["corner2"].y=@y+@height-16 + @sprites["corner3"].x=@x+@width-16 + @sprites["corner3"].y=@y+@height-16 + @sprites["side0"].x=@x+16 + @sprites["side0"].y=@y + @sprites["side1"].x=@x + @sprites["side1"].y=@y+16 + @sprites["side2"].x=@x+@width-16 + @sprites["side2"].y=@y+16 + @sprites["side3"].x=@x+16 + @sprites["side3"].y=@y+@height-16 + @sprites["scroll0"].x = @x+@width / 2 - 8 + @sprites["scroll0"].y = @y+8 + @sprites["scroll1"].x = @x+8 + @sprites["scroll1"].y = @y+@height / 2 - 8 + @sprites["scroll2"].x = @x+@width - 16 + @sprites["scroll2"].y = @y+@height / 2 - 8 + @sprites["scroll3"].x = @x+@width / 2 - 8 + @sprites["scroll3"].y = @y+@height - 16 + @sprites["back"].x=@x+2 + @sprites["back"].y=@y+2 + @sprites["cursor"].x=@x+16+@cursor_rect.x + @sprites["cursor"].y=@y+16+@cursor_rect.y + if changeBitmap && @_windowskin && !@_windowskin.disposed? + width=@cursor_rect.width + height=@cursor_rect.height + if width > 0 && height > 0 + cursorrects=[ + # sides + Rect.new(cursorX+2, cursorY+0, 28, 2), + Rect.new(cursorX+0, cursorY+2, 2, 28), + Rect.new(cursorX+30, cursorY+2, 2, 28), + Rect.new(cursorX+2, cursorY+30, 28, 2), + # corners + Rect.new(cursorX+0, cursorY+0, 2, 2), + Rect.new(cursorX+30, cursorY+0, 2, 2), + Rect.new(cursorX+0, cursorY+30, 2, 2), + Rect.new(cursorX+30, cursorY+30, 2, 2), + # back + Rect.new(cursorX+2, cursorY+2, 28, 28) + ] + margin=2 + fullmargin=4 + @cursorbitmap = ensureBitmap(@cursorbitmap, width, height) + @cursorbitmap.clear + @sprites["cursor"].bitmap=@cursorbitmap + @sprites["cursor"].src_rect.set(0,0,width,height) + rect = Rect.new(margin,margin, + width - fullmargin, height - fullmargin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[8]) + @cursorbitmap.blt(0, 0, @_windowskin, cursorrects[4])# top left + @cursorbitmap.blt(width-margin, 0, @_windowskin, cursorrects[5]) # top right + @cursorbitmap.blt(0, height-margin, @_windowskin, cursorrects[6]) # bottom right + @cursorbitmap.blt(width-margin, height-margin, @_windowskin, cursorrects[7]) # bottom left + rect = Rect.new(margin, 0, + width - fullmargin, margin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[0]) + rect = Rect.new(0, margin, + margin, height - fullmargin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[1]) + rect = Rect.new(width - margin, margin, + margin, height - fullmargin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[2]) + rect = Rect.new(margin, height-margin, + width - fullmargin, margin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[3]) + else + @sprites["cursor"].visible=false + @sprites["cursor"].src_rect.set(0,0,0,0) + end + for i in 0...4 + dwidth = (i==0 || i==3) ? @width-32 : 16 + dheight = (i==0 || i==3) ? 16 : @height-32 + @sidebitmaps[i]=ensureBitmap(@sidebitmaps[i],dwidth,dheight) + @sprites["side#{i}"].bitmap=@sidebitmaps[i] + @sprites["side#{i}"].src_rect.set(0,0,dwidth,dheight) + @sidebitmaps[i].clear + if sideRects[i].width>0 && sideRects[i].height>0 + @sidebitmaps[i].stretch_blt(@sprites["side#{i}"].src_rect, + @_windowskin,sideRects[i]) + end + end + backwidth=@width-4 + backheight=@height-4 + if backwidth>0 && backheight>0 + @backbitmap=ensureBitmap(@backbitmap,backwidth,backheight) + @sprites["back"].bitmap=@backbitmap + @sprites["back"].src_rect.set(0,0,backwidth,backheight) + @backbitmap.clear + if @stretch + @backbitmap.stretch_blt(@sprites["back"].src_rect,@_windowskin,backRect) + else + tileBitmap(@backbitmap,@sprites["back"].src_rect,@_windowskin,backRect) + end + if blindsRect + tileBitmap(@backbitmap,@sprites["back"].src_rect,@_windowskin,blindsRect) + end + else + @sprites["back"].visible=false + @sprites["back"].src_rect.set(0,0,0,0) + end + end + if @openness!=255 + opn=@openness/255.0 + for k in @spritekeys + sprite=@sprites[k] + ratio=(@height<=0) ? 0 : (sprite.y-@y)*1.0/@height + sprite.zoom_y=opn + sprite.oy=0 + sprite.y=(@y+(@height/2.0)+(@height*ratio*opn)-(@height/2*opn)).floor + end + else + for k in @spritekeys + sprite=@sprites[k] + sprite.zoom_y=1.0 + end + end + i=0 + # Ensure Z order + for k in @spritekeys + sprite=@sprites[k] + y=sprite.y + sprite.y=i + sprite.oy=(sprite.zoom_y<=0) ? 0 : (i-y)/sprite.zoom_y + end + end +end diff --git a/Data/Scripts/007_Objects and windows/004_SpriteWindow.rb b/Data/Scripts/007_Objects and windows/004_SpriteWindow.rb new file mode 100644 index 000000000..0188cd94e --- /dev/null +++ b/Data/Scripts/007_Objects and windows/004_SpriteWindow.rb @@ -0,0 +1,936 @@ +#=============================================================================== +# SpriteWindow is a class based on Window which emulates Window's functionality. +# This class is necessary in order to change the viewport of windows (with +# viewport=) and to make windows fade in and out (with tone=). +#=============================================================================== +class SpriteWindow < Window + attr_reader :tone + attr_reader :color + attr_reader :viewport + attr_reader :contents + attr_reader :ox + attr_reader :oy + attr_reader :x + attr_reader :y + attr_reader :z + attr_reader :zoom_x + attr_reader :zoom_y + attr_reader :offset_x + attr_reader :offset_y + attr_reader :width + attr_reader :active + attr_reader :pause + attr_reader :height + attr_reader :opacity + attr_reader :back_opacity + attr_reader :contents_opacity + attr_reader :visible + attr_reader :cursor_rect + attr_reader :contents_blend_type + attr_reader :blend_type + attr_reader :openness + + def windowskin + @_windowskin + end + + # Flags used to preserve compatibility + # with RGSS/RGSS2's version of Window + module CompatBits + CorrectZ = 1 + ExpandBack = 2 + ShowScrollArrows = 4 + StretchSides = 8 + ShowPause = 16 + ShowCursor = 32 + end + + attr_reader :compat + + def compat=(value) + @compat=value + privRefresh(true) + end + + def initialize(viewport=nil) + @sprites={} + @spritekeys=[ + "back", + "corner0","side0","scroll0", + "corner1","side1","scroll1", + "corner2","side2","scroll2", + "corner3","side3","scroll3", + "cursor","contents","pause" + ] + @viewport=viewport + @sidebitmaps=[nil,nil,nil,nil] + @cursorbitmap=nil + @bgbitmap=nil + for i in @spritekeys + @sprites[i]=Sprite.new(@viewport) + end + @disposed=false + @tone=Tone.new(0,0,0) + @color=Color.new(0,0,0,0) + @blankcontents=Bitmap.new(1,1) # RGSS2 requires this + @contents=@blankcontents + @_windowskin=nil + @rpgvx=false + @compat=CompatBits::ExpandBack|CompatBits::StretchSides + @x=0 + @y=0 + @width=0 + @height=0 + @offset_x=0 + @offset_y=0 + @zoom_x=1.0 + @zoom_y=1.0 + @ox=0 + @oy=0 + @z=0 + @stretch=true + @visible=true + @active=true + @openness=255 + @opacity=255 + @back_opacity=255 + @blend_type=0 + @contents_blend_type=0 + @contents_opacity=255 + @cursor_rect=WindowCursorRect.new(self) + @cursorblink=0 + @cursoropacity=255 + @pause=false + @pauseframe=0 + @flash=0 + @pauseopacity=0 + @skinformat=0 + @skinrect=Rect.new(0,0,0,0) + @trim=[16,16,16,16] + privRefresh(true) + end + + def dispose + if !self.disposed? + for i in @sprites + i[1].dispose if i[1] + @sprites[i[0]]=nil + end + for i in 0...@sidebitmaps.length + @sidebitmaps[i].dispose if @sidebitmaps[i] + @sidebitmaps[i]=nil + end + @blankcontents.dispose + @cursorbitmap.dispose if @cursorbitmap + @backbitmap.dispose if @backbitmap + @sprites.clear + @sidebitmaps.clear + @_windowskin=nil + @disposed=true + end + end + + def stretch=(value) + @stretch=value + privRefresh(true) + end + + def visible=(value) + @visible=value + privRefresh + end + + def viewport=(value) + @viewport=value + for i in @spritekeys + @sprites[i].dispose if @sprites[i] + end + for i in @spritekeys + if @sprites[i].is_a?(Sprite) + @sprites[i]=Sprite.new(@viewport) + else + @sprites[i]=nil + end + end + privRefresh(true) + end + + def z=(value) + @z=value + privRefresh + end + + def disposed? + return @disposed + end + + def contents=(value) + if @contents!=value + @contents=value + privRefresh if @visible + end + end + + def ox=(value) + if @ox!=value + @ox=value + privRefresh if @visible + end + end + + def oy=(value) + if @oy!=value + @oy=value + privRefresh if @visible + end + end + + def active=(value) + @active=value + privRefresh(true) + end + + def cursor_rect=(value) + if !value + @cursor_rect.empty + else + @cursor_rect.set(value.x,value.y,value.width,value.height) + end + end + + def openness=(value) + @openness=value + @openness=0 if @openness<0 + @openness=255 if @openness>255 + privRefresh + end + + def width=(value) + @width=value + privRefresh(true) + end + + def height=(value) + @height=value + privRefresh(true) + end + + def pause=(value) + @pause=value + @pauseopacity=0 if !value + privRefresh if @visible + end + + def x=(value) + @x=value + privRefresh if @visible + end + + def y=(value) + @y=value + privRefresh if @visible + end + + def zoom_x=(value) + @zoom_x=value + privRefresh if @visible + end + + def zoom_y=(value) + @zoom_y=value + privRefresh if @visible + end + + def offset_x=(value) + @x=value + privRefresh if @visible + end + + def offset_y=(value) + @y=value + privRefresh if @visible + end + + def opacity=(value) + @opacity=value + @opacity=0 if @opacity<0 + @opacity=255 if @opacity>255 + privRefresh if @visible + end + + def back_opacity=(value) + @back_opacity=value + @back_opacity=0 if @back_opacity<0 + @back_opacity=255 if @back_opacity>255 + privRefresh if @visible + end + + def contents_opacity=(value) + @contents_opacity=value + @contents_opacity=0 if @contents_opacity<0 + @contents_opacity=255 if @contents_opacity>255 + privRefresh if @visible + end + + def tone=(value) + @tone=value + privRefresh if @visible + end + + def color=(value) + @color=value + privRefresh if @visible + end + + def blend_type=(value) + @blend_type=value + privRefresh if @visible + end + + def flash(color,duration) + return if disposed? + @flash=duration+1 + for i in @sprites + i[1].flash(color,duration) + end + end + + def update + return if disposed? + mustchange=false + if @active + if @cursorblink==0 + @cursoropacity-=8 + @cursorblink=1 if @cursoropacity<=128 + else + @cursoropacity+=8 + @cursorblink=0 if @cursoropacity>=255 + end + privRefreshCursor + else + @cursoropacity=128 + privRefreshCursor + end + if @pause + oldpauseframe=@pauseframe + oldpauseopacity=@pauseopacity + @pauseframe=(Graphics.frame_count / 8) % 4 + @pauseopacity=[@pauseopacity+64,255].min + mustchange=@pauseframe!=oldpauseframe || @pauseopacity!=oldpauseopacity + end + privRefresh if mustchange + if @flash>0 + for i in @sprites.values + i.update + end + @flash-=1 + end + end + + ############# + attr_reader :skinformat + attr_reader :skinrect + + def loadSkinFile(_file) + if (self.windowskin.width==80 || self.windowskin.width==96) && + self.windowskin.height==48 + # Body = X, Y, width, height of body rectangle within windowskin + @skinrect.set(32,16,16,16) + # Trim = X, Y, width, height of trim rectangle within windowskin + @trim=[32,16,16,16] + elsif self.windowskin.width==80 && self.windowskin.height==80 + @skinrect.set(32,32,16,16) + @trim=[32,16,16,48] + end + end + + def windowskin=(value) + oldSkinWidth=(@_windowskin && !@_windowskin.disposed?) ? @_windowskin.width : -1 + oldSkinHeight=(@_windowskin && !@_windowskin.disposed?) ? @_windowskin.height : -1 + @_windowskin=value + if @skinformat==1 + @rpgvx=false + if @_windowskin && !@_windowskin.disposed? + if @_windowskin.width!=oldSkinWidth || @_windowskin.height!=oldSkinHeight + # Update skinrect and trim if windowskin's dimensions have changed + @skinrect.set((@_windowskin.width-16)/2,(@_windowskin.height-16)/2,16,16) + @trim=[@skinrect.x,@skinrect.y,@skinrect.x,@skinrect.y] + end + else + @skinrect.set(16,16,16,16) + @trim=[16,16,16,16] + end + else + if value && value.is_a?(Bitmap) && !value.disposed? && value.width==128 + @rpgvx=true + else + @rpgvx=false + end + @trim=[16,16,16,16] + end + privRefresh(true) + end + + def skinrect=(value) + @skinrect=value + privRefresh + end + + def skinformat=(value) + if @skinformat!=value + @skinformat=value + privRefresh(true) + end + end + + def borderX + return 32 if !@trim || skinformat==0 + if @_windowskin && !@_windowskin.disposed? + return @trim[0]+(@_windowskin.width-@trim[2]-@trim[0]) + end + return 32 + end + + def borderY + return 32 if !@trim || skinformat==0 + if @_windowskin && !@_windowskin.disposed? + return @trim[1]+(@_windowskin.height-@trim[3]-@trim[1]) + end + return 32 + end + + def leftEdge; self.startX; end + def topEdge; self.startY; end + def rightEdge; self.borderX-self.leftEdge; end + def bottomEdge; self.borderY-self.topEdge; end + + def startX + return !@trim || skinformat==0 ? 16 : @trim[0] + end + + def startY + return !@trim || skinformat==0 ? 16 : @trim[1] + end + + def endX + return !@trim || skinformat==0 ? 16 : @trim[2] + end + + def endY + return !@trim || skinformat==0 ? 16 : @trim[3] + end + + def startX=(value) + @trim[0]=value + privRefresh + end + + def startY=(value) + @trim[1]=value + privRefresh + end + + def endX=(value) + @trim[2]=value + privRefresh + end + + def endY=(value) + @trim[3]=value + privRefresh + end + + ############# + private + + def ensureBitmap(bitmap,dwidth,dheight) + if !bitmap||bitmap.disposed?||bitmap.width0 && @skinformat==0 && !@rpgvx + # Compatibility Mode: Cursor, pause, and contents have higher Z + @sprites["cursor"].z=@z+1 + @sprites["contents"].z=@z+2 + @sprites["pause"].z=@z+2 + end + if @skinformat==0 + startX=16 + startY=16 + endX=16 + endY=16 + trimStartX=16 + trimStartY=16 + trimWidth=32 + trimHeight=32 + if @rpgvx + trimX=64 + trimY=0 + backRect=Rect.new(0,0,64,64) + blindsRect=Rect.new(0,64,64,64) + else + trimX=128 + trimY=0 + backRect=Rect.new(0,0,128,128) + blindsRect=nil + end + if @_windowskin && !@_windowskin.disposed? + @sprites["corner0"].src_rect.set(trimX,trimY+0,16,16); + @sprites["corner1"].src_rect.set(trimX+48,trimY+0,16,16); + @sprites["corner2"].src_rect.set(trimX,trimY+48,16,16); + @sprites["corner3"].src_rect.set(trimX+48,trimY+48,16,16); + @sprites["scroll0"].src_rect.set(trimX+24, trimY+16, 16, 8) # up + @sprites["scroll3"].src_rect.set(trimX+24, trimY+40, 16, 8) # down + @sprites["scroll1"].src_rect.set(trimX+16, trimY+24, 8, 16) # left + @sprites["scroll2"].src_rect.set(trimX+40, trimY+24, 8, 16) # right + cursorX=trimX + cursorY=trimY+64 + sideRects=[ + Rect.new(trimX+16,trimY+0,32,16), + Rect.new(trimX,trimY+16,16,32), + Rect.new(trimX+48,trimY+16,16,32), + Rect.new(trimX+16,trimY+48,32,16) + ] + pauseRects=[ + trimX+32,trimY+64, + trimX+48,trimY+64, + trimX+32,trimY+80, + trimX+48,trimY+80, + ] + pauseWidth=16 + pauseHeight=16 + @sprites["pause"].src_rect.set( + pauseRects[@pauseframe*2], + pauseRects[@pauseframe*2+1], + pauseWidth,pauseHeight + ) + end + else + trimStartX=@trim[0] + trimStartY=@trim[1] + trimWidth=@trim[0]+(@skinrect.width-@trim[2]+@trim[0]) + trimHeight=@trim[1]+(@skinrect.height-@trim[3]+@trim[1]) + if @_windowskin && !@_windowskin.disposed? + # width of left end of window + startX=@skinrect.x + # width of top end of window + startY=@skinrect.y + cx=@skinrect.x+@skinrect.width # right side of BODY rect + cy=@skinrect.y+@skinrect.height # bottom side of BODY rect + # width of right end of window + endX=(!@_windowskin || @_windowskin.disposed?) ? @skinrect.x : @_windowskin.width-cx + # height of bottom end of window + endY=(!@_windowskin || @_windowskin.disposed?) ? @skinrect.y : @_windowskin.height-cy + @sprites["corner0"].src_rect.set(0,0,startX,startY); + @sprites["corner1"].src_rect.set(cx,0,endX,startY); + @sprites["corner2"].src_rect.set(0,cy,startX,endY); + @sprites["corner3"].src_rect.set(cx,cy,endX,endY); + backRect=Rect.new(@skinrect.x,@skinrect.y, + @skinrect.width,@skinrect.height); + blindsRect=nil + sideRects=[ + Rect.new(startX,0,@skinrect.width,startY), # side0 (top) + Rect.new(0,startY,startX,@skinrect.height), # side1 (left) + Rect.new(cx,startY,endX,@skinrect.height), # side2 (right) + Rect.new(startX,cy,@skinrect.width,endY) # side3 (bottom) + ] + end + end + if @width>trimWidth && @height>trimHeight + @sprites["contents"].src_rect.set(@ox,@oy,@width-trimWidth,@height-trimHeight) + else + @sprites["contents"].src_rect.set(0,0,0,0) + end + @sprites["contents"].x=@x+trimStartX + @sprites["contents"].y=@y+trimStartY + if (@compat & CompatBits::ShowScrollArrows)>0 && @skinformat==0 + # Compatibility mode: Make scroll arrows visible + if @skinformat==0 && @_windowskin && !@_windowskin.disposed? && + @contents && !@contents.disposed? + @sprites["scroll0"].visible = @visible && hascontents && @oy > 0 + @sprites["scroll1"].visible = @visible && hascontents && @ox > 0 + @sprites["scroll2"].visible = @visible && (@contents.width - @ox) > @width-trimWidth + @sprites["scroll3"].visible = @visible && (@contents.height - @oy) > @height-trimHeight + end + end + if @_windowskin && !@_windowskin.disposed? + borderX=startX+endX + borderY=startY+endY + @sprites["corner0"].x=@x + @sprites["corner0"].y=@y + @sprites["corner1"].x=@x+@width-endX + @sprites["corner1"].y=@y + @sprites["corner2"].x=@x + @sprites["corner2"].y=@y+@height-endY + @sprites["corner3"].x=@x+@width-endX + @sprites["corner3"].y=@y+@height-endY + @sprites["side0"].x=@x+startX + @sprites["side0"].y=@y + @sprites["side1"].x=@x + @sprites["side1"].y=@y+startY + @sprites["side2"].x=@x+@width-endX + @sprites["side2"].y=@y+startY + @sprites["side3"].x=@x+startX + @sprites["side3"].y=@y+@height-endY + @sprites["scroll0"].x = @x+@width / 2 - 8 + @sprites["scroll0"].y = @y+8 + @sprites["scroll1"].x = @x+8 + @sprites["scroll1"].y = @y+@height / 2 - 8 + @sprites["scroll2"].x = @x+@width - 16 + @sprites["scroll2"].y = @y+@height / 2 - 8 + @sprites["scroll3"].x = @x+@width / 2 - 8 + @sprites["scroll3"].y = @y+@height - 16 + @sprites["cursor"].x=@x+startX+@cursor_rect.x + @sprites["cursor"].y=@y+startY+@cursor_rect.y + if (@compat & CompatBits::ExpandBack)>0 && @skinformat==0 + # Compatibility mode: Expand background + @sprites["back"].x=@x+2 + @sprites["back"].y=@y+2 + else + @sprites["back"].x=@x+startX + @sprites["back"].y=@y+startY + end + end + if changeBitmap && @_windowskin && !@_windowskin.disposed? + if @skinformat==0 + @sprites["cursor"].x=@x+startX+@cursor_rect.x + @sprites["cursor"].y=@y+startY+@cursor_rect.y + width=@cursor_rect.width + height=@cursor_rect.height + if width > 0 && height > 0 + cursorrects=[ + # sides + Rect.new(cursorX+2, cursorY+0, 28, 2), + Rect.new(cursorX+0, cursorY+2, 2, 28), + Rect.new(cursorX+30, cursorY+2, 2, 28), + Rect.new(cursorX+2, cursorY+30, 28, 2), + # corners + Rect.new(cursorX+0, cursorY+0, 2, 2), + Rect.new(cursorX+30, cursorY+0, 2, 2), + Rect.new(cursorX+0, cursorY+30, 2, 2), + Rect.new(cursorX+30, cursorY+30, 2, 2), + # back + Rect.new(cursorX+2, cursorY+2, 28, 28) + ] + margin=2 + fullmargin=4 + @cursorbitmap = ensureBitmap(@cursorbitmap, width, height) + @cursorbitmap.clear + @sprites["cursor"].bitmap=@cursorbitmap + @sprites["cursor"].src_rect.set(0,0,width,height) + rect = Rect.new(margin,margin,width - fullmargin, height - fullmargin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[8]) + @cursorbitmap.blt(0, 0, @_windowskin, cursorrects[4])# top left + @cursorbitmap.blt(width-margin, 0, @_windowskin, cursorrects[5]) # top right + @cursorbitmap.blt(0, height-margin, @_windowskin, cursorrects[6]) # bottom right + @cursorbitmap.blt(width-margin, height-margin, @_windowskin, cursorrects[7]) # bottom left + rect = Rect.new(margin, 0,width - fullmargin, margin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[0]) + rect = Rect.new(0, margin,margin, height - fullmargin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[1]) + rect = Rect.new(width - margin, margin, margin, height - fullmargin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[2]) + rect = Rect.new(margin, height-margin, width - fullmargin, margin) + @cursorbitmap.stretch_blt(rect, @_windowskin, cursorrects[3]) + else + @sprites["cursor"].visible=false + @sprites["cursor"].src_rect.set(0,0,0,0) + end + end + for i in 0..3 + case i + when 0 + dwidth = @width-startX-endX + dheight = startY + when 1 + dwidth = startX + dheight = @height-startY-endY + when 2 + dwidth = endX + dheight = @height-startY-endY + when 3 + dwidth = @width-startX-endX + dheight = endY + end + @sidebitmaps[i]=ensureBitmap(@sidebitmaps[i],dwidth,dheight) + @sprites["side#{i}"].bitmap=@sidebitmaps[i] + @sprites["side#{i}"].src_rect.set(0,0,dwidth,dheight) + @sidebitmaps[i].clear + if sideRects[i].width>0 && sideRects[i].height>0 + if (@compat & CompatBits::StretchSides)>0 && @skinformat==0 + # Compatibility mode: Stretch sides + @sidebitmaps[i].stretch_blt(@sprites["side#{i}"].src_rect, + @_windowskin,sideRects[i]) + else + tileBitmap(@sidebitmaps[i],@sprites["side#{i}"].src_rect, + @_windowskin,sideRects[i]) + end + end + end + if (@compat & CompatBits::ExpandBack)>0 && @skinformat==0 + # Compatibility mode: Expand background + backwidth=@width-4 + backheight=@height-4 + else + backwidth=@width-borderX + backheight=@height-borderY + end + if backwidth>0 && backheight>0 + @backbitmap=ensureBitmap(@backbitmap,backwidth,backheight) + @sprites["back"].bitmap=@backbitmap + @sprites["back"].src_rect.set(0,0,backwidth,backheight) + @backbitmap.clear + if @stretch + @backbitmap.stretch_blt(@sprites["back"].src_rect,@_windowskin,backRect) + else + tileBitmap(@backbitmap,@sprites["back"].src_rect,@_windowskin,backRect) + end + if blindsRect + tileBitmap(@backbitmap,@sprites["back"].src_rect,@_windowskin,blindsRect) + end + else + @sprites["back"].visible=false + @sprites["back"].src_rect.set(0,0,0,0) + end + end + if @openness!=255 + opn=@openness/255.0 + for k in @spritekeys + sprite=@sprites[k] + ratio=(@height<=0) ? 0 : (sprite.y-@y)*1.0/@height + sprite.zoom_y=opn + sprite.zoom_x=1.0 + sprite.oy=0 + sprite.y=(@y+(@height/2.0)+(@height*ratio*opn)-(@height/2*opn)).floor + end + else + for k in @spritekeys + sprite=@sprites[k] + sprite.zoom_x=1.0 + sprite.zoom_y=1.0 + end + end + i=0 + # Ensure Z order + for k in @spritekeys + sprite=@sprites[k] + y=sprite.y + sprite.y=i + sprite.oy=(sprite.zoom_y<=0) ? 0 : (i-y)/sprite.zoom_y + sprite.zoom_x*=@zoom_x + sprite.zoom_y*=@zoom_y + sprite.x*=@zoom_x + sprite.y*=@zoom_y + sprite.x+=(@offset_x/sprite.zoom_x) + sprite.y+=(@offset_y/sprite.zoom_y) + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +class SpriteWindow_Base < SpriteWindow + TEXTPADDING=4 # In pixels + + def initialize(x, y, width, height) + super() + self.x = x + self.y = y + self.width = width + self.height = height + self.z = 100 + @curframe=MessageConfig.pbGetSystemFrame() + @curfont=MessageConfig.pbGetSystemFontName() + @sysframe=AnimatedBitmap.new(@curframe) + RPG::Cache.retain(@curframe) if @curframe && !@curframe.empty? + @customskin=nil + __setWindowskin(@sysframe.bitmap) + __resolveSystemFrame() + pbSetSystemFont(self.contents) if self.contents + end + + def __setWindowskin(skin) + if skin && (skin.width==192 && skin.height==128) || # RPGXP Windowskin + (skin.width==128 && skin.height==128) # RPGVX Windowskin + self.skinformat=0 + else + self.skinformat=1 + end + self.windowskin=skin + end + + def __resolveSystemFrame + if self.skinformat==1 + if !@resolvedFrame + @resolvedFrame=MessageConfig.pbGetSystemFrame() + @resolvedFrame.sub!(/\.[^\.\/\\]+$/,"") + end + self.loadSkinFile("#{@resolvedFrame}.txt") if @resolvedFrame!="" + end + end + + def setSkin(skin) # Filename of windowskin to apply. Supports XP, VX, and animated skins. + @customskin.dispose if @customskin + @customskin=nil + resolvedName=pbResolveBitmap(skin) + return if nil_or_empty?(resolvedName) + @customskin=AnimatedBitmap.new(resolvedName) + RPG::Cache.retain(resolvedName) + __setWindowskin(@customskin.bitmap) + if self.skinformat==1 + skinbase=resolvedName.sub(/\.[^\.\/\\]+$/,"") + self.loadSkinFile("#{skinbase}.txt") + end + end + + def setSystemFrame + @customskin.dispose if @customskin + @customskin=nil + __setWindowskin(@sysframe.bitmap) + __resolveSystemFrame() + end + + def update + super + if self.windowskin + if @customskin + if @customskin.totalFrames>1 + @customskin.update + __setWindowskin(@customskin.bitmap) + end + elsif @sysframe + if @sysframe.totalFrames>1 + @sysframe.update + __setWindowskin(@sysframe.bitmap) + end + end + end + if @curframe!=MessageConfig.pbGetSystemFrame() + @curframe=MessageConfig.pbGetSystemFrame() + if @sysframe && !@customskin + @sysframe.dispose if @sysframe + @sysframe=AnimatedBitmap.new(@curframe) + RPG::Cache.retain(@curframe) if @curframe && !@curframe.empty? + @resolvedFrame=nil + __setWindowskin(@sysframe.bitmap) + __resolveSystemFrame() + end + begin + refresh + rescue NoMethodError + end + end + if @curfont!=MessageConfig.pbGetSystemFontName() + @curfont=MessageConfig.pbGetSystemFontName() + if self.contents && !self.contents.disposed? + pbSetSystemFont(self.contents) + end + begin + refresh + rescue NoMethodError + end + end + end + + def dispose + self.contents.dispose if self.contents + @sysframe.dispose + @customskin.dispose if @customskin + super + end +end diff --git a/Data/Scripts/007_Objects and windows/005_SpriteWindow_text.rb b/Data/Scripts/007_Objects and windows/005_SpriteWindow_text.rb new file mode 100644 index 000000000..a05882c85 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/005_SpriteWindow_text.rb @@ -0,0 +1,1401 @@ +#=============================================================================== +# +#=============================================================================== +# Represents a window with no formatting capabilities. Its text color can be set, +# though, and line breaks are supported, but the text is generally unformatted. +class Window_UnformattedTextPokemon < SpriteWindow_Base + attr_reader :text + attr_reader :baseColor + attr_reader :shadowColor + # Letter-by-letter mode. This mode is not supported in this class. + attr_accessor :letterbyletter + + def text=(value) + @text=value + refresh + end + + def baseColor=(value) + @baseColor=value + refresh + end + + def shadowColor=(value) + @shadowColor=value + refresh + end + + def initialize(text="") + super(0,0,33,33) + self.contents=Bitmap.new(1,1) + pbSetSystemFont(self.contents) + @text=text + @letterbyletter=false # Not supported in this class + colors=getDefaultTextColors(self.windowskin) + @baseColor=colors[0] + @shadowColor=colors[1] + resizeToFit(text) + end + + def self.newWithSize(text,x,y,width,height,viewport=nil) + ret=self.new(text) + ret.x=x + ret.y=y + ret.width=width + ret.height=height + ret.viewport=viewport + ret.refresh + return ret + end + + def resizeToFitInternal(text,maxwidth) # maxwidth is maximum acceptable window width + dims=[0,0] + cwidth=maxwidth<0 ? Graphics.width : maxwidth + getLineBrokenChunks(self.contents,text, + cwidth-self.borderX-SpriteWindow_Base::TEXTPADDING,dims,true) + return dims + end + + def setTextToFit(text,maxwidth=-1) + resizeToFit(text,maxwidth) + self.text=text + end + + def resizeToFit(text, maxwidth = -1) # maxwidth is maximum acceptable window width + dims = resizeToFitInternal(text,maxwidth) + self.width = dims[0] + self.borderX + SpriteWindow_Base::TEXTPADDING + self.height = dims[1] + self.borderY + refresh + end + + def resizeHeightToFit(text, width = -1) # width is current window width + dims = resizeToFitInternal(text,width) + self.width = (width < 0) ? Graphics.width : width + self.height = dims[1] + self.borderY + refresh + end + + def setSkin(skin) + super(skin) + privRefresh(true) + oldbaser = @baseColor.red + oldbaseg = @baseColor.green + oldbaseb = @baseColor.blue + oldbasea = @baseColor.alpha + oldshadowr = @shadowColor.red + oldshadowg = @shadowColor.green + oldshadowb = @shadowColor.blue + oldshadowa = @shadowColor.alpha + colors = getDefaultTextColors(self.windowskin) + @baseColor = colors[0] + @shadowColor = colors[1] + if oldbaser!=@baseColor.red || oldbaseg!=@baseColor.green || + oldbaseb!=@baseColor.blue || oldbasea!=@baseColor.alpha || + oldshadowr!=@shadowColor.red || oldshadowg!=@shadowColor.green || + oldshadowb!=@shadowColor.blue || oldshadowa!=@shadowColor.alpha + self.text = self.text + end + end + + def refresh + self.contents=pbDoEnsureBitmap(self.contents,self.width-self.borderX, + self.height-self.borderY) + self.contents.clear + drawTextEx(self.contents,0,4,self.contents.width,0, + @text.gsub(/\r/,""),@baseColor,@shadowColor) + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_AdvancedTextPokemon < SpriteWindow_Base + attr_reader :text + attr_reader :baseColor + attr_reader :shadowColor + attr_accessor :letterbyletter + attr_reader :waitcount + + def initialize(text="") + @cursorMode = MessageConfig::CURSOR_POSITION + @endOfText = nil + @scrollstate = 0 + @realframes = 0 + @scrollY = 0 + @nodraw = false + @lineHeight = 32 + @linesdrawn = 0 + @bufferbitmap = nil + @letterbyletter = false + @starting = true + @displaying = false + @lastDrawnChar = -1 + @fmtchars = [] + @frameskipChanged = false + @frameskip = MessageConfig.pbGetTextSpeed() + super(0,0,33,33) + @pausesprite = nil + @text = "" + self.contents = Bitmap.new(1,1) + pbSetSystemFont(self.contents) + self.resizeToFit(text,Graphics.width) + colors = getDefaultTextColors(self.windowskin) + @baseColor = colors[0] + @shadowColor = colors[1] + self.text = text + @starting = false + end + + def self.newWithSize(text,x,y,width,height,viewport=nil) + ret = self.new(text) + ret.x = x + ret.y = y + ret.width = width + ret.height = height + ret.viewport = viewport + return ret + end + + def dispose + return if disposed? + @pausesprite.dispose if @pausesprite + @pausesprite = nil + super + end + + def waitcount=(value) + @waitcount = (value<=0) ? 0 : value + end + + attr_reader :cursorMode + + def cursorMode=(value) + @cursorMode = value + moveCursor + end + + def lineHeight(value) + @lineHeight = value + self.text = self.text + end + + def baseColor=(value) + @baseColor = value + refresh + end + + def shadowColor=(value) + @shadowColor = value + refresh + end + + def textspeed + @frameskip + end + + def textspeed=(value) + @frameskipChanged = true if @frameskip!=value + @frameskip = value + end + + def width=(value) + super + self.text = self.text if !@starting + end + + def height=(value) + super + self.text = self.text if !@starting + end + + def resizeToFit(text,maxwidth=-1) + dims = resizeToFitInternal(text,maxwidth) + oldstarting = @starting + @starting = true + self.width = dims[0]+self.borderX+SpriteWindow_Base::TEXTPADDING + self.height = dims[1]+self.borderY + @starting = oldstarting + redrawText + end + + def resizeToFit2(text,maxwidth,maxheight) + dims = resizeToFitInternal(text,maxwidth) + oldstarting = @starting + @starting = true + self.width = [dims[0]+self.borderX+SpriteWindow_Base::TEXTPADDING,maxwidth].min + self.height = [dims[1]+self.borderY,maxheight].min + @starting = oldstarting + redrawText + end + + def resizeToFitInternal(text,maxwidth) + dims = [0,0] + cwidth = (maxwidth<0) ? Graphics.width : maxwidth + chars = getFormattedTextForDims(self.contents,0,0, + cwidth-self.borderX-2-6,-1,text,@lineHeight,true) + for ch in chars + dims[0] = [dims[0],ch[1]+ch[3]].max + dims[1] = [dims[1],ch[2]+ch[4]].max + end + return dims + end + + def resizeHeightToFit(text,width=-1) + dims = resizeToFitInternal(text,width) + oldstarting = @starting + @starting = true + self.width = (width < 0) ? Graphics.width : width + self.height = dims[1] + self.borderY + @starting = oldstarting + redrawText + end + + def setSkin(skin,redrawText=true) + super(skin) + privRefresh(true) + oldbaser = @baseColor.red + oldbaseg = @baseColor.green + oldbaseb = @baseColor.blue + oldbasea = @baseColor.alpha + oldshadowr = @shadowColor.red + oldshadowg = @shadowColor.green + oldshadowb = @shadowColor.blue + oldshadowa = @shadowColor.alpha + colors = getDefaultTextColors(self.windowskin) + @baseColor = colors[0] + @shadowColor = colors[1] + if redrawText && + (oldbaser!=@baseColor.red || oldbaseg!=@baseColor.green || + oldbaseb!=@baseColor.blue || oldbasea!=@baseColor.alpha || + oldshadowr!=@shadowColor.red || oldshadowg!=@shadowColor.green || + oldshadowb!=@shadowColor.blue || oldshadowa!=@shadowColor.alpha) + setText(self.text) + end + end + + def setTextToFit(text,maxwidth=-1) + resizeToFit(text,maxwidth) + self.text = text + end + + def text=(value) + setText(value) + end + + def setText(value) + @waitcount = 0 + @curchar = 0 + @drawncurchar = -1 + @lastDrawnChar = -1 + @text = value + @textlength = unformattedTextLength(value) + @scrollstate = 0 + @scrollY = 0 + @linesdrawn = 0 + @realframes = 0 + @textchars = [] + width = 1 + height = 1 + numlines = 0 + visiblelines = (self.height-self.borderY)/32 + if value.length==0 + @fmtchars = [] + @bitmapwidth = width + @bitmapheight = height + @numtextchars = 0 + else + if @letterbyletter + @fmtchars = [] + fmt = getFormattedText(self.contents,0,0, + self.width-self.borderX-SpriteWindow_Base::TEXTPADDING,-1, + shadowctag(@baseColor,@shadowColor)+value,32,true) + @oldfont = self.contents.font.clone + for ch in fmt + chx = ch[1]+ch[3] + chy = ch[2]+ch[4] + width = chx if width=visiblelines + fclone = ch.clone + fclone[0] = "\1" + @fmtchars.push(fclone) + @textchars.push("\1") + end + end + # Don't add newline characters, since they + # can slow down letter-by-letter display + if ch[5] || (ch[0]!="\r") + @fmtchars.push(ch) + @textchars.push(ch[5] ? "" : ch[0]) + end + end + fmt.clear + else + @fmtchars = getFormattedText(self.contents,0,0, + self.width-self.borderX-SpriteWindow_Base::TEXTPADDING,-1, + shadowctag(@baseColor,@shadowColor)+value,32,true) + @oldfont = self.contents.font.clone + for ch in @fmtchars + chx = ch[1]+ch[3] + chy = ch[2]+ch[4] + width = chx if width=@fmtchars.length + # index after the last character's index + return @fmtchars[@lastDrawnChar][14]+1 + end + + def maxPosition + pos = 0 + for ch in @fmtchars + # index after the last character's index + pos = ch[14]+1 if pos=@fmtchars.length # End of message + if @textchars[@curchar]=="\1" # Pause message + @pausing = true if @curchar<@numtextchars-1 + self.startPause + refresh + break + end + break if @textchars[@curchar]!="\n" # Skip past newlines only + break if @linesdrawn>=visiblelines-1 # No more empty lines to continue to + @linesdrawn += 1 + end + end + + def allocPause + return if @pausesprite + @pausesprite = AnimatedSprite.create("Graphics/Pictures/pause",4,3) + @pausesprite.z = 100000 + @pausesprite.visible = false + end + + def startPause + allocPause + @pausesprite.visible = true + @pausesprite.frame = 0 + @pausesprite.start + moveCursor + end + + def stopPause + return if !@pausesprite + @pausesprite.stop + @pausesprite.visible = false + end + + def moveCursor + return if !@pausesprite + cursor = @cursorMode + cursor = 2 if cursor==0 && !@endOfText + case cursor + when 0 # End of text + @pausesprite.x = self.x+self.startX+@endOfText.x+@endOfText.width-2 + @pausesprite.y = self.y+self.startY+@endOfText.y-@scrollY + when 1 # Lower right + pauseWidth = @pausesprite.bitmap ? @pausesprite.framewidth : 16 + pauseHeight = @pausesprite.bitmap ? @pausesprite.frameheight : 16 + @pausesprite.x = self.x+self.width-(20*2)+(pauseWidth/2) + @pausesprite.y = self.y+self.height-(30*2)+(pauseHeight/2) + when 2 # Lower middle + pauseWidth = @pausesprite.bitmap ? @pausesprite.framewidth : 16 + pauseHeight = @pausesprite.bitmap ? @pausesprite.frameheight : 16 + @pausesprite.x = self.x+(self.width/2)-(pauseWidth/2) + @pausesprite.y = self.y+self.height-(18*2)+(pauseHeight/2) + end + end + + def refresh + oldcontents = self.contents + self.contents = pbDoEnsureBitmap(oldcontents,@bitmapwidth,@bitmapheight) + self.oy = @scrollY + numchars = @numtextchars + numchars = [@curchar,@numtextchars].min if self.letterbyletter + return if busy? && @drawncurchar==@curchar && @scrollstate==0 + if !self.letterbyletter || !oldcontents.equal?(self.contents) + @drawncurchar = -1 + @needclear = true + end + if @needclear + self.contents.font = @oldfont if @oldfont + self.contents.clear + @needclear = false + end + if @nodraw + @nodraw = false + return + end + maxX = self.width-self.borderX + maxY = self.height-self.borderY + for i in @drawncurchar+1..numchars + next if i>=@fmtchars.length + if !self.letterbyletter + next if @fmtchars[i][1]>=maxX + next if @fmtchars[i][2]>=maxY + end + drawSingleFormattedChar(self.contents,@fmtchars[i]) + @lastDrawnChar = i + end + if !self.letterbyletter + # all characters were drawn, reset old font + self.contents.font = @oldfont if @oldfont + end + if numchars>0 && numchars!=@numtextchars + fch = @fmtchars[numchars-1] + if fch + rcdst = Rect.new(fch[1],fch[2],fch[3],fch[4]) + if @textchars[numchars]=="\1" + @endOfText = rcdst + allocPause + moveCursor + else + @endOfText = Rect.new(rcdst.x+rcdst.width,rcdst.y,8,1) + end + end + end + @drawncurchar = @curchar + end + + def redrawText + if @letterbyletter + oldPosition = self.position + self.text = self.text + oldPosition = @numtextchars if oldPosition>@numtextchars + while self.position!=oldPosition + refresh + updateInternal + end + else + self.text = self.text + end + end + + def updateInternal + curcharskip = @frameskip<0 ? @frameskip.abs : 1 + visiblelines = (self.height-self.borderY)/@lineHeight + if @textchars[@curchar]=="\1" + if !@pausing + @realframes += 1 + if @realframes>=@frameskip || @frameskip<0 + curcharSkip(curcharskip) + @realframes = 0 + end + end + elsif @textchars[@curchar]=="\n" + if @linesdrawn>=visiblelines-1 + if @scrollstate<@lineHeight + @scrollstate += [(@lineHeight/4),1].max + @scrollY += [(@lineHeight/4),1].max + end + if @scrollstate>=@lineHeight + @realframes += 1 + if @realframes>=@frameskip || @frameskip<0 + curcharSkip(curcharskip) + @linesdrawn += 1 + @realframes = 0 + @scrollstate = 0 + end + end + else + @realframes += 1 + if @realframes>=@frameskip || @frameskip<0 + curcharSkip(curcharskip) + @linesdrawn += 1 + @realframes = 0 + end + end + elsif @curchar<=@numtextchars + @realframes += 1 + if @realframes>=@frameskip || @frameskip<0 + curcharSkip(curcharskip) + @realframes = 0 + end + if @textchars[@curchar]=="\1" + @pausing = true if @curchar<@numtextchars-1 + self.startPause + refresh + end + else + @displaying = false + @scrollstate = 0 + @scrollY = 0 + @linesdrawn = 0 + end + end + + def update + super + @pausesprite.update if @pausesprite && @pausesprite.visible + if @waitcount>0 + @waitcount -= 1 + return + end + if busy? + refresh if !@frameskipChanged + updateInternal + # following line needed to allow "textspeed=-999" to work seamlessly + refresh if @frameskipChanged + end + @frameskipChanged = false + end + + private + + def curcharSkip(skip) + skip.times do + @curchar += 1 + break if @textchars[@curchar]=="\n" || # newline + @textchars[@curchar]=="\1" || # pause + @textchars[@curchar]=="\2" || # letter-by-letter break + @textchars[@curchar]==nil + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_InputNumberPokemon < SpriteWindow_Base + attr_reader :sign + + def initialize(digits_max) + @digits_max=digits_max + @number=0 + @frame=0 + @sign=false + @negative=false + super(0,0,32,32) + self.width=digits_max*24+8+self.borderX + self.height=32+self.borderY + colors=getDefaultTextColors(self.windowskin) + @baseColor=colors[0] + @shadowColor=colors[1] + @index=digits_max-1 + self.active=true + refresh + end + + def active=(value) + super + refresh + end + + def number + @number*(@sign && @negative ? -1 : 1) + end + + def number=(value) + value=0 if !value.is_a?(Numeric) + if @sign + @negative=(value<0) + @number = [value.abs, 10 ** @digits_max - 1].min + else + @number = [[value, 0].max, 10 ** @digits_max - 1].min + end + refresh + end + + def sign=(value) + @sign=value + self.width=@digits_max*24+8+self.borderX+(@sign ? 24 : 0) + @index=(@digits_max-1)+(@sign ? 1 : 0) + refresh + end + + def refresh + self.contents=pbDoEnsureBitmap(self.contents, + self.width-self.borderX,self.height-self.borderY) + pbSetSystemFont(self.contents) + self.contents.clear + s=sprintf("%0*d",@digits_max,@number.abs) + if @sign + textHelper(0,0,@negative ? "-" : "+",0) + end + for i in 0...@digits_max + index=i+(@sign ? 1 : 0) + textHelper(index*24,0,s[i,1],index) + end + end + + def update + super + digits=@digits_max+(@sign ? 1 : 0) + refresh if @frame%15==0 + if self.active + if Input.repeat?(Input::UP) || Input.repeat?(Input::DOWN) + pbPlayCursorSE() + if @index==0 && @sign + @negative=!@negative + else + place = 10 ** (digits - 1 - @index) + n = @number / place % 10 + @number -= n*place + if Input.repeat?(Input::UP) + n = (n + 1) % 10 + elsif Input.repeat?(Input::DOWN) + n = (n + 9) % 10 + end + @number += n*place + end + refresh + elsif Input.repeat?(Input::RIGHT) + if digits >= 2 + pbPlayCursorSE() + @index = (@index + 1) % digits + @frame=0 + refresh + end + elsif Input.repeat?(Input::LEFT) + if digits >= 2 + pbPlayCursorSE() + @index = (@index + digits - 1) % digits + @frame=0 + refresh + end + end + end + @frame=(@frame+1)%30 + end + + private + + def textHelper(x,y,text,i) + textwidth=self.contents.text_size(text).width + pbDrawShadowText(self.contents, x+(12-textwidth/2), y, textwidth+4, 32, text, @baseColor, @shadowColor) + if @index==i && @active && @frame/15==0 + self.contents.fill_rect(x+(12-textwidth/2), y+30, textwidth, 2, @baseColor) + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +class SpriteWindow_Selectable < SpriteWindow_Base + attr_reader :index + + def initialize(x, y, width, height) + super(x, y, width, height) + @item_max = 1 + @column_max = 1 + @virtualOy=0 + @index = -1 + @row_height = 32 + @column_spacing = 32 + @ignore_input = false + @allow_arrows_jump=false + end + + def itemCount + return @item_max || 0 + end + + def index=(index) + if @index!=index + @index = index + priv_update_cursor_rect(true) + end + end + + def rowHeight + return @row_height || 32 + end + + def rowHeight=(value) + if @row_height!=value + oldTopRow=self.top_row + @row_height=[1,value].max + self.top_row=oldTopRow + update_cursor_rect + end + end + + def setAllowArrowsJump(value) + @allow_arrows_jump=value + end + + def columns + return @column_max || 1 + end + + def columns=(value) + if @column_max!=value + @column_max=[1,value].max + update_cursor_rect + end + end + + def columnSpacing + return @column_spacing || 32 + end + + def columnSpacing=(value) + if @column_spacing!=value + @column_spacing=[0,value].max + update_cursor_rect + end + end + + def ignore_input=(value) + @ignore_input=value + end + + def count + return @item_max + end + + def row_max + return ((@item_max + @column_max - 1) / @column_max).to_i + end + + def top_row + return (@virtualOy / (@row_height || 32)).to_i + end + + def top_row=(row) + row = row_max-1 if row>row_max-1 + row = 0 if row<0 + @virtualOy = row*@row_height + end + + def top_item + return top_row * @column_max + end + + def page_row_max + return priv_page_row_max.to_i + end + + def page_item_max + return priv_page_item_max.to_i + end + + def itemRect(item) + if item<0 || item>=@item_max || itemself.top_item+self.page_item_max + return Rect.new(0,0,0,0) + else + cursor_width = (self.width-self.borderX-(@column_max-1)*@column_spacing) / @column_max + x = item % @column_max * (cursor_width + @column_spacing) + y = item / @column_max * @row_height - @virtualOy + return Rect.new(x, y, cursor_width, @row_height) + end + end + + def refresh; end + + def changedPosition; end + + def update_cursor_rect + priv_update_cursor_rect + changedPosition + end + + def update + super + if self.active && @item_max > 0 && @index >= 0 && !@ignore_input + if Input.repeat?(Input::UP) + scroll_up() + elsif Input.repeat?(Input::DOWN) + scroll_down() + elsif Input.repeat?(Input::LEFT) && !@allow_arrows_jump + scroll_left() + elsif Input.repeat?(Input::RIGHT) && !@allow_arrows_jump + scroll_right() + elsif Input.repeat?(Input::JUMPUP) || (Input.repeat?(Input::LEFT) && @allow_arrows_jump) + jump_up() + elsif Input.repeat?(Input::JUMPDOWN) || (Input.repeat?(Input::RIGHT) && @allow_arrows_jump) + jump_down() + end + end + end + + def scroll_up() + if @index >= @column_max || + (Input.trigger?(Input::UP) && (@item_max % @column_max)==0) + oldindex = @index + @index = (@index - @column_max + @item_max) % @item_max + if @index!=oldindex + pbPlayCursorSE() + update_cursor_rect + end + end + end + + def scroll_down() + if @index < @item_max - @column_max || + (Input.trigger?(Input::DOWN) && (@item_max % @column_max)==0) + oldindex = @index + @index = (@index + @column_max) % @item_max + if @index!=oldindex + pbPlayCursorSE() + update_cursor_rect + end + end + end + + def scroll_left() + if @column_max >= 2 && @index < @item_max - 1 + oldindex = @index + @index += 1 + if @index!=oldindex + pbPlayCursorSE() + update_cursor_rect + end + end + end + + def scroll_right() + if @column_max >= 2 && @index > 0 + oldindex = @index + @index -= 1 + if @index!=oldindex + pbPlayCursorSE() + update_cursor_rect + end + end + end + + + def jump_up() + if @index > 0 + oldindex = @index + @index = [self.index-self.page_item_max, 0].max + if @index!=oldindex + pbPlayCursorSE() + self.top_row -= self.page_row_max + update_cursor_rect + end + end + end + + + def jump_down() + if @index < @item_max-1 + oldindex = @index + @index = [self.index+self.page_item_max, @item_max-1].min + if @index!=oldindex + pbPlayCursorSE() + self.top_row += self.page_row_max + update_cursor_rect + end + end + end + + private + + def priv_page_row_max + return (self.height - self.borderY) / @row_height + end + + def priv_page_item_max + return (self.height - self.borderY) / @row_height * @column_max + end + + def priv_update_cursor_rect(force=false) + if @index < 0 + self.cursor_rect.empty + self.refresh + return + end + dorefresh = false + row = @index / @column_max + # This code makes lists scroll only when the cursor hits the top and bottom + # of the visible list. +# if row < self.top_row +# self.top_row = row +# dorefresh=true +# end +# if row > self.top_row + (self.page_row_max - 1) +# self.top_row = row - (self.page_row_max - 1) +# dorefresh=true +# end +# if oldindex-self.top_item>=((self.page_item_max - 1)/2) +# self.top_row+=1 +# end +# self.top_row = [self.top_row, self.row_max - self.page_row_max].min + # This code makes the cursor stay in the middle of the visible list as much + # as possible. + new_top_row = row - ((self.page_row_max - 1)/2).floor + new_top_row = [[new_top_row, self.row_max - self.page_row_max].min, 0].max + if self.top_row != new_top_row + self.top_row = new_top_row +# dorefresh = true + end + # End of code + cursor_width = (self.width-self.borderX) / @column_max + x = self.index % @column_max * (cursor_width + @column_spacing) + y = self.index / @column_max * @row_height - @virtualOy + self.cursor_rect.set(x, y, cursor_width, @row_height) + self.refresh if dorefresh || force + end +end + + + +#=============================================================================== +# +#=============================================================================== +module UpDownArrowMixin + def initUpDownArrow + @uparrow = AnimatedSprite.create("Graphics/Pictures/uparrow",8,2,self.viewport) + @downarrow = AnimatedSprite.create("Graphics/Pictures/downarrow",8,2,self.viewport) + RPG::Cache.retain("Graphics/Pictures/uparrow") + RPG::Cache.retain("Graphics/Pictures/downarrow") + @uparrow.z = 99998 + @downarrow.z = 99998 + @uparrow.visible = false + @downarrow.visible = false + @uparrow.play + @downarrow.play + end + + def dispose + @uparrow.dispose + @downarrow.dispose + super + end + + def viewport=(value) + super + @uparrow.viewport = self.viewport + @downarrow.viewport = self.viewport + end + + def color=(value) + super + @uparrow.color = value + @downarrow.color = value + end + + def adjustForZoom(sprite) + sprite.zoom_x = self.zoom_x + sprite.zoom_y = self.zoom_y + sprite.x = sprite.x*self.zoom_x + self.offset_x/self.zoom_x + sprite.y = sprite.y*self.zoom_y + self.offset_y/self.zoom_y + end + + def update + super + @uparrow.x = self.x+(self.width/2)-(@uparrow.framewidth/2) + @downarrow.x = self.x+(self.width/2)-(@downarrow.framewidth/2) + @uparrow.y = self.y + @downarrow.y = self.y+self.height-@downarrow.frameheight + @uparrow.visible = self.visible && self.active && (self.top_item!=0 && + @item_max > self.page_item_max) + @downarrow.visible = self.visible && self.active && + (self.top_item+self.page_item_max<@item_max && @item_max > self.page_item_max) + @uparrow.z = self.z+1 + @downarrow.z = self.z+1 + adjustForZoom(@uparrow) + adjustForZoom(@downarrow) + @uparrow.viewport = self.viewport + @downarrow.viewport = self.viewport + @uparrow.update + @downarrow.update + end +end + + + +#=============================================================================== +# +#=============================================================================== +class SpriteWindow_SelectableEx < SpriteWindow_Selectable + include UpDownArrowMixin + + def initialize(*arg) + super(*arg) + initUpDownArrow + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_DrawableCommand < SpriteWindow_SelectableEx + attr_reader :baseColor + attr_reader :shadowColor + attr_reader :index + + def initialize(x,y,width,height,viewport=nil) + super(x,y,width,height) + self.viewport = viewport if viewport + if isDarkWindowskin(self.windowskin) + @selarrow = AnimatedBitmap.new("Graphics/Pictures/selarrow_white") + RPG::Cache.retain("Graphics/Pictures/selarrow_white") + else + @selarrow = AnimatedBitmap.new("Graphics/Pictures/selarrow") + RPG::Cache.retain("Graphics/Pictures/selarrow") + end + @index = 0 + colors = getDefaultTextColors(self.windowskin) + @baseColor = Color.new(150, 150, 150)#colors[0] + @shadowColor = colors[1] + refresh + end + + + def dispose + @selarrow.dispose + super + end + + def baseColor=(value) + @baseColor = value + refresh + end + + def shadowColor=(value) + @shadowColor = value + refresh + end + + def textWidth(bitmap,text) + return bitmap.text_size(text).width + end + + def getAutoDims(commands,dims,width=nil) + rowMax = ((commands.length + self.columns - 1) / self.columns).to_i + windowheight = (rowMax*self.rowHeight) + windowheight += self.borderY + if !width || width<0 + width=0 + tmpbitmap = BitmapWrapper.new(1,1) + pbSetSystemFont(tmpbitmap) + for i in commands + width = [width,tmpbitmap.text_size(i).width].max + end + # one 16 to allow cursor + width += 16+16+SpriteWindow_Base::TEXTPADDING + tmpbitmap.dispose + end + # Store suggested width and height of window + dims[0] = [self.borderX+1,(width*self.columns)+self.borderX+ + (self.columns-1)*self.columnSpacing].max + dims[1] = [self.borderY+1,windowheight].max + dims[1] = [dims[1],Graphics.height].min + end + + def setSkin(skin) + super(skin) + privRefresh(true) + colors = getDefaultTextColors(self.windowskin) + @baseColor = colors[0] + @shadowColor = colors[1] + end + + def drawCursor(index,rect) + if self.index==index + pbCopyBitmap(self.contents,@selarrow.bitmap,rect.x,rect.y) + end + return Rect.new(rect.x+16,rect.y,rect.width-16,rect.height) + end + + def itemCount # to be implemented by derived classes + return 0 + end + + def drawItem(index,count,rect) # to be implemented by derived classes + end + + def refresh + @item_max = itemCount() + dwidth = self.width-self.borderX + dheight = self.height-self.borderY + self.contents = pbDoEnsureBitmap(self.contents,dwidth,dheight) + self.contents.clear + for i in 0...@item_max + next if iself.top_item+self.page_item_max + drawItem(i,@item_max,itemRect(i)) + end + end + + def update + oldindex = self.index + super + refresh if self.index!=oldindex + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_CommandPokemon < Window_DrawableCommand + attr_reader :commands + + def initialize(commands,width=nil) + @starting=true + @commands=[] + dims=[] + super(0,0,32,32) + getAutoDims(commands,dims,width) + self.width=dims[0] + self.height=dims[1] + @commands=commands + self.active=true + colors=getDefaultTextColors(self.windowskin) + self.baseColor=colors[0] + self.shadowColor=colors[1] + refresh + @starting=false + end + + def self.newWithSize(commands,x,y,width,height,viewport=nil) + ret=self.new(commands,width) + ret.x=x + ret.y=y + ret.width=width + ret.height=height + ret.viewport=viewport + return ret + end + + def self.newEmpty(x,y,width,height,viewport=nil) + ret=self.new([],width) + ret.x=x + ret.y=y + ret.width=width + ret.height=height + ret.viewport=viewport + return ret + end + + def index=(value) + super + refresh if !@starting + end + + def commands=(value) + @commands=value + @item_max=commands.length + self.update_cursor_rect + self.refresh + end + + def width=(value) + super + if !@starting + self.index=self.index + self.update_cursor_rect + end + end + + def height=(value) + super + if !@starting + self.index=self.index + self.update_cursor_rect + end + end + + def resizeToFit(commands,width=nil) + dims=[] + getAutoDims(commands,dims,width) + self.width=dims[0] + self.height=dims[1] + end + + def itemCount + return @commands ? @commands.length : 0 + end + + def drawItem(index,_count,rect) + pbSetSystemFont(self.contents) if @starting + rect=drawCursor(index,rect) + pbDrawShadowText(self.contents,rect.x,rect.y,rect.width,rect.height, + @commands[index],self.baseColor,self.shadowColor) + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_CommandPokemonEx < Window_CommandPokemon +end + + +#=============================================================================== +# +#=============================================================================== +class Window_AdvancedCommandPokemon < Window_DrawableCommand + attr_reader :commands + + def textWidth(bitmap,text) + dims=[nil,0] + chars=getFormattedText(bitmap,0,0, + Graphics.width-self.borderX-SpriteWindow_Base::TEXTPADDING-16, + -1,text,self.rowHeight,true,true) + for ch in chars + dims[0]=dims[0] ? [dims[0],ch[1]].min : ch[1] + dims[1]=[dims[1],ch[1]+ch[3]].max + end + dims[0]=0 if !dims[0] + return dims[1]-dims[0] + end + + def initialize(commands,width=nil) + @starting=true + @commands=[] + dims=[] + super(0,0,32,32) + getAutoDims(commands,dims,width) + self.width=dims[0] + self.height=dims[1] + @commands=commands + self.active=true + colors=getDefaultTextColors(self.windowskin) + self.baseColor=colors[0] + self.shadowColor=colors[1] + refresh + @starting=false + end + + def self.newWithSize(commands,x,y,width,height,viewport=nil) + ret=self.new(commands,width) + ret.x=x + ret.y=y + ret.width=width + ret.height=height + ret.viewport=viewport + return ret + end + + def self.newEmpty(x,y,width,height,viewport=nil) + ret=self.new([],width) + ret.x=x + ret.y=y + ret.width=width + ret.height=height + ret.viewport=viewport + return ret + end + + def index=(value) + super + refresh if !@starting + end + + def commands=(value) + @commands=value + @item_max=commands.length + self.update_cursor_rect + self.refresh + end + + def width=(value) + oldvalue=self.width + super + if !@starting && oldvalue!=value + self.index=self.index + self.update_cursor_rect + end + end + + def height=(value) + oldvalue=self.height + super + if !@starting && oldvalue!=value + self.index=self.index + self.update_cursor_rect + end + end + + def resizeToFit(commands,width=nil) + dims=[] + getAutoDims(commands,dims,width) + self.width=dims[0] + self.height=dims[1] - 6 + end + + def itemCount + return @commands ? @commands.length : 0 + end + + def drawItem(index,_count,rect) + pbSetSystemFont(self.contents) + rect=drawCursor(index,rect) + if toUnformattedText(@commands[index]).gsub(/\n/,"")==@commands[index] + # Use faster alternative for unformatted text without line breaks + pbDrawShadowText(self.contents,rect.x,rect.y,rect.width,rect.height, + @commands[index],self.baseColor,self.shadowColor) + else + chars=getFormattedText( + self.contents,rect.x,rect.y+4,rect.width,rect.height, + @commands[index],rect.height,true,true) + drawFormattedChars(self.contents,chars) + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_AdvancedCommandPokemonEx < Window_AdvancedCommandPokemon +end diff --git a/Data/Scripts/007_Objects and windows/006_SpriteWindow_pictures.rb b/Data/Scripts/007_Objects and windows/006_SpriteWindow_pictures.rb new file mode 100644 index 000000000..f9ce0aed1 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/006_SpriteWindow_pictures.rb @@ -0,0 +1,123 @@ +#=============================================================================== +# Displays an icon bitmap in a window. Supports animated images. +#=============================================================================== +class IconWindow < SpriteWindow_Base + attr_reader :name + + def initialize(x,y,width,height,viewport=nil) + super(x,y,width,height) + self.viewport=viewport + self.contents=nil + @name="" + @_iconbitmap=nil + end + + def dispose + clearBitmaps() + super + end + + def update + super + if @_iconbitmap + @_iconbitmap.update + self.contents=@_iconbitmap.bitmap + end + end + + def clearBitmaps + @_iconbitmap.dispose if @_iconbitmap + @_iconbitmap=nil + self.contents=nil if !self.disposed? + end + + # Sets the icon's filename. Alias for setBitmap. + def name=(value) + setBitmap(value) + end + + # Sets the icon's filename. + def setBitmap(file,hue=0) + clearBitmaps() + @name=file + return if file==nil + if file!="" + @_iconbitmap=AnimatedBitmap.new(file,hue) + # for compatibility + self.contents=@_iconbitmap ? @_iconbitmap.bitmap : nil + else + @_iconbitmap=nil + end + end +end + + + +#=============================================================================== +# Displays an icon bitmap in a window. Supports animated images. +# Accepts bitmaps and paths to bitmap files in its constructor. +#=============================================================================== +class PictureWindow < SpriteWindow_Base + def initialize(pathOrBitmap) + super(0,0,32,32) + self.viewport=viewport + self.contents=nil + @_iconbitmap=nil + setBitmap(pathOrBitmap) + end + + def picture; @_iconbitmap; end + + def dispose + clearBitmaps() + super + end + + def update + super + if @_iconbitmap + if @_iconbitmap.is_a?(Bitmap) + self.contents=@_iconbitmap + else + @_iconbitmap.update + self.contents=@_iconbitmap.bitmap + end + end + end + + def clearBitmaps + @_iconbitmap.dispose if @_iconbitmap + @_iconbitmap=nil + self.contents=nil if !self.disposed? + end + + # Sets the icon's bitmap or filename. (hue parameter + # is ignored unless pathOrBitmap is a filename) + def setBitmap(pathOrBitmap,hue=0) + clearBitmaps() + if pathOrBitmap!=nil && pathOrBitmap!="" + if pathOrBitmap.is_a?(Bitmap) + @_iconbitmap=pathOrBitmap + self.contents=@_iconbitmap + self.width=@_iconbitmap.width+self.borderX + self.height=@_iconbitmap.height+self.borderY + elsif pathOrBitmap.is_a?(AnimatedBitmap) + @_iconbitmap=pathOrBitmap + self.contents=@_iconbitmap.bitmap + self.width=@_iconbitmap.bitmap.width+self.borderX + self.height=@_iconbitmap.bitmap.height+self.borderY + else + @_iconbitmap=AnimatedBitmap.new(pathOrBitmap,hue) + self.contents=@_iconbitmap ? @_iconbitmap.bitmap : nil + self.width=@_iconbitmap ? @_iconbitmap.bitmap.width+self.borderX : + 32+self.borderX + self.height=@_iconbitmap ? @_iconbitmap.bitmap.height+self.borderY : + 32+self.borderY + end + else + @_iconbitmap=nil + self.width=32+self.borderX + self.height=32+self.borderY + end + end +end diff --git a/Data/Scripts/007_Objects and windows/007_SpriteWrapper.rb b/Data/Scripts/007_Objects and windows/007_SpriteWrapper.rb new file mode 100644 index 000000000..9541069f7 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/007_SpriteWrapper.rb @@ -0,0 +1,496 @@ +#=============================================================================== +# SpriteWrapper is a class which wraps (most of) Sprite's properties. +#=============================================================================== +class SpriteWrapper + def initialize(viewport = nil) + @sprite = Sprite.new(viewport) + end + + def dispose + @sprite.dispose; + end + + def disposed? + return @sprite.disposed?; + end + + def viewport + return @sprite.viewport; + end + + def flash(color, duration) + ; return @sprite.flash(color, duration); + end + + def update + return @sprite.update; + end + + def x + @sprite.x; + end + + def x=(value) + ; @sprite.x = value; + end + + def y + @sprite.y; + end + + def y=(value) + ; @sprite.y = value; + end + + def bitmap + @sprite.bitmap; + end + + def bitmap=(value) + ; @sprite.bitmap = value; + end + + def src_rect + @sprite.src_rect; + end + + def src_rect=(value) + ; @sprite.src_rect = value; + end + + def visible + @sprite.visible; + end + + def visible=(value) + ; @sprite.visible = value; + end + + def z + @sprite.z; + end + + def z=(value) + ; @sprite.z = value; + end + + def ox + @sprite.ox; + end + + def ox=(value) + ; @sprite.ox = value; + end + + def oy + @sprite.oy; + end + + def oy=(value) + ; @sprite.oy = value; + end + + def zoom_x + @sprite.zoom_x; + end + + def zoom_x=(value) + ; @sprite.zoom_x = value; + end + + def zoom_y + @sprite.zoom_y; + end + + def zoom_y=(value) + ; @sprite.zoom_y = value; + end + + def angle + @sprite.angle; + end + + def angle=(value) + ; @sprite.angle = value; + end + + def mirror + @sprite.mirror; + end + + def mirror=(value) + ; @sprite.mirror = value; + end + + def bush_depth + @sprite.bush_depth; + end + + def bush_depth=(value) + ; @sprite.bush_depth = value; + end + + def opacity + @sprite.opacity; + end + + def opacity=(value) + ; @sprite.opacity = value; + end + + def blend_type + @sprite.blend_type; + end + + def blend_type=(value) + ; @sprite.blend_type = value; + end + + def color + @sprite.color; + end + + def color=(value) + ; @sprite.color = value; + end + + def tone + @sprite.tone; + end + + def tone=(value) + ; @sprite.tone = value; + end + + def viewport=(value) + return if self.viewport == value + bitmap = @sprite.bitmap + src_rect = @sprite.src_rect + visible = @sprite.visible + x = @sprite.x + y = @sprite.y + z = @sprite.z + ox = @sprite.ox + oy = @sprite.oy + zoom_x = @sprite.zoom_x + zoom_y = @sprite.zoom_y + angle = @sprite.angle + mirror = @sprite.mirror + bush_depth = @sprite.bush_depth + opacity = @sprite.opacity + blend_type = @sprite.blend_type + color = @sprite.color + tone = @sprite.tone + @sprite.dispose + @sprite = Sprite.new(value) + @sprite.bitmap = bitmap + @sprite.src_rect = src_rect + @sprite.visible = visible + @sprite.x = x + @sprite.y = y + @sprite.z = z + @sprite.ox = ox + @sprite.oy = oy + @sprite.zoom_x = zoom_x + @sprite.zoom_y = zoom_y + @sprite.angle = angle + @sprite.mirror = mirror + @sprite.bush_depth = bush_depth + @sprite.opacity = opacity + @sprite.blend_type = blend_type + @sprite.color = color + @sprite.tone = tone + end +end + +#=============================================================================== +# Sprite class that maintains a bitmap of its own. +# This bitmap can't be changed to a different one. +#=============================================================================== +class BitmapSprite < SpriteWrapper + def initialize(width, height, viewport = nil) + super(viewport) + self.bitmap = Bitmap.new(width, height) + @initialized = true + end + + def bitmap=(value) + super(value) if !@initialized + end + + def dispose + self.bitmap.dispose if !self.disposed? + super + end +end + +#=============================================================================== +# +#=============================================================================== +class AnimatedSprite < SpriteWrapper + attr_reader :frame + attr_reader :framewidth + attr_reader :frameheight + attr_reader :framecount + attr_reader :animname + attr_reader :playing + + def initializeLong(animname, framecount, framewidth, frameheight, frameskip) + @animname = pbBitmapName(animname) + @realframes = 0 + @frameskip = [1, frameskip].max + @frameskip *= Graphics.frame_rate / 20 + raise _INTL("Frame width is 0") if framewidth == 0 + raise _INTL("Frame height is 0") if frameheight == 0 + begin + @animbitmap = AnimatedBitmap.new(animname).deanimate + rescue + @animbitmap = Bitmap.new(framewidth, frameheight) + end + if @animbitmap.width % framewidth != 0 + raise _INTL("Bitmap's width ({1}) is not a multiple of frame width ({2}) [Bitmap={3}]", + @animbitmap.width, framewidth, animname) + end + if @animbitmap.height % frameheight != 0 + raise _INTL("Bitmap's height ({1}) is not a multiple of frame height ({2}) [Bitmap={3}]", + @animbitmap.height, frameheight, animname) + end + @framecount = framecount + @framewidth = framewidth + @frameheight = frameheight + @framesperrow = @animbitmap.width / @framewidth + @playing = false + self.bitmap = @animbitmap + self.src_rect.width = @framewidth + self.src_rect.height = @frameheight + self.frame = 0 + end + + # Shorter version of AnimationSprite. All frames are placed on a single row + # of the bitmap, so that the width and height need not be defined beforehand + def initializeShort(animname, framecount, frameskip) + @animname = pbBitmapName(animname) + @realframes = 0 + @frameskip = [1, frameskip].max + @frameskip *= Graphics.frame_rate / 20 + begin + @animbitmap = AnimatedBitmap.new(animname).deanimate + rescue + @animbitmap = Bitmap.new(framecount * 4, 32) + end + if @animbitmap.width % framecount != 0 + raise _INTL("Bitmap's width ({1}) is not a multiple of frame count ({2}) [Bitmap={3}]", + @animbitmap.width, framewidth, animname) + end + @framecount = framecount + @framewidth = @animbitmap.width / @framecount + @frameheight = @animbitmap.height + @framesperrow = framecount + @playing = false + self.bitmap = @animbitmap + self.src_rect.width = @framewidth + self.src_rect.height = @frameheight + self.frame = 0 + end + + def initialize(*args) + if args.length == 1 + super(args[0][3]) + initializeShort(args[0][0], args[0][1], args[0][2]) + else + super(args[5]) + initializeLong(args[0], args[1], args[2], args[3], args[4]) + end + end + + def self.create(animname, framecount, frameskip, viewport = nil) + return self.new([animname, framecount, frameskip, viewport]) + end + + def dispose + return if disposed? + @animbitmap.dispose + @animbitmap = nil + super + end + + def playing? + return @playing + end + + def frame=(value) + @frame = value + @realframes = 0 + self.src_rect.x = @frame % @framesperrow * @framewidth + self.src_rect.y = @frame / @framesperrow * @frameheight + end + + def start + @playing = true + @realframes = 0 + end + + alias play start + + def stop + @playing = false + end + + def reset + @frame=0 + @realframes = 0 + end + + def update + super + if @playing + @realframes += 1 + if @realframes == @frameskip + @realframes = 0 + self.frame += 1 + self.frame %= self.framecount + end + end + end +end + +#=============================================================================== +# Displays an icon bitmap in a sprite. Supports animated images. +#=============================================================================== +class IconSprite < SpriteWrapper + attr_reader :name + def initialize(*args) + if args.length == 0 + super(nil) + self.bitmap = nil + elsif args.length == 1 + super(args[0]) + self.bitmap = nil + elsif args.length == 2 + super(nil) + self.x = args[0] + self.y = args[1] + else + super(args[2]) + self.x = args[0] + self.y = args[1] + end + @name = "" + @_iconbitmap = nil + end + + def dispose + clearBitmaps() + super + end + + # Sets the icon's filename. Alias for setBitmap. + def name=(value) + setBitmap(value) + end + + def setBitmapDirectly(bitmap) + oldrc = self.src_rect + clearBitmaps() + @name = "" + return if bitmap == nil + @_iconbitmap = bitmap + # for compatibility + # + self.bitmap = @_iconbitmap ? @_iconbitmap.bitmap : nil + self.src_rect = oldrc + end + + def setColor(r = 0, g = 0, b = 0, a = 255) + @_iconbitmap.pbSetColor(r,g,b,a) + end + + # Sets the icon's filename. + def setBitmap(file, hue = 0) + oldrc = self.src_rect + clearBitmaps() + @name = file + return if file == nil + if file != "" + @_iconbitmap = AnimatedBitmap.new(file, hue) + # for compatibility + self.bitmap = @_iconbitmap ? @_iconbitmap.bitmap : nil + self.src_rect = oldrc + else + @_iconbitmap = nil + end + end + + def getBitmap + return @_iconbitmap + end + + def clearBitmaps + @_iconbitmap.dispose if @_iconbitmap + @_iconbitmap = nil + self.bitmap = nil if !self.disposed? + end + + def update + super + return if !@_iconbitmap + @_iconbitmap.update + if self.bitmap != @_iconbitmap.bitmap + oldrc = self.src_rect + self.bitmap = @_iconbitmap.bitmap + self.src_rect = oldrc + end + end + + +end + +#=============================================================================== +# Old GifSprite class, retained for compatibility +#=============================================================================== +class GifSprite < IconSprite + def initialize(path) + super(0, 0) + setBitmap(path) + end +end + +#=============================================================================== +# SpriteWrapper that stores multiple bitmaps, and displays only one at once. +#=============================================================================== +class ChangelingSprite < SpriteWrapper + def initialize(x = 0, y = 0, viewport = nil) + super(viewport) + self.x = x + self.y = y + @bitmaps = {} + @currentBitmap = nil + end + + def addBitmap(key, path) + @bitmaps[key].dispose if @bitmaps[key] + @bitmaps[key] = AnimatedBitmap.new(path) + end + + def changeBitmap(key) + @currentBitmap = @bitmaps[key] + self.bitmap = (@currentBitmap) ? @currentBitmap.bitmap : nil + end + + def dispose + return if disposed? + for bm in @bitmaps.values; + bm.dispose; + end + @bitmaps.clear + super + end + + def update + return if disposed? + for bm in @bitmaps.values; + bm.update; + end + self.bitmap = (@currentBitmap) ? @currentBitmap.bitmap : nil + end +end diff --git a/Data/Scripts/007_Objects and windows/008_AnimatedBitmap.rb b/Data/Scripts/007_Objects and windows/008_AnimatedBitmap.rb new file mode 100644 index 000000000..e9d737529 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/008_AnimatedBitmap.rb @@ -0,0 +1,360 @@ +#=============================================================================== +# +#=============================================================================== + +class AnimatedBitmap + attr_reader :path + attr_reader :filename + + def initialize(file, hue = 0) + raise "Filename is nil (missing graphic)." if file.nil? + path = file + filename = "" + if file.last != '/' # Isn't just a directory + split_file = file.split(/[\\\/]/) + filename = split_file.pop + path = split_file.join('/') + '/' + end + @filename = filename + @path = path + if filename[/^\[\d+(?:,\d+)?\]/] # Starts with 1 or 2 numbers in square brackets + @bitmap = PngAnimatedBitmap.new(path, filename, hue) + else + @bitmap = GifBitmap.new(path, filename, hue) + end + end + + def setup_from_bitmap(bitmap,hue=0) + @path = "" + @filename = "" + @bitmap = GifBitmap.new("", '', hue) + @bitmap.bitmap = bitmap; + end + + def self.from_bitmap(bitmap, hue=0) + obj = allocate + obj.send(:setup_from_bitmap, bitmap, hue) + obj + end + + def pbSetColor(r = 0, g = 0, b = 0, a = 255) + color = Color.new(r, g, b, a) + pbSetColorValue(color) + end + + def pbSetColorValue(color) + for i in 0..@bitmap.bitmap.width + for j in 0..@bitmap.bitmap.height + if @bitmap.bitmap.get_pixel(i, j).alpha != 0 + @bitmap.bitmap.set_pixel(i, j, color) + end + end + end + end + + + def shiftColors(offset = 0) + @bitmap.bitmap.hue_change(offset) + end + + def [](index) + ; @bitmap[index]; + end + + def width + @bitmap.bitmap.width; + end + + def height + @bitmap.bitmap.height; + end + + def length + @bitmap.length; + end + + def each + @bitmap.each { |item| yield item }; + end + + def bitmap + @bitmap.bitmap; + end + + def bitmap=(bitmap) + @bitmap.bitmap = bitmap; + end + + def currentIndex + @bitmap.currentIndex; + end + + def totalFrames + @bitmap.totalFrames; + end + + def disposed? + @bitmap.disposed?; + end + + def update + @bitmap.update; + end + + def dispose + @bitmap.dispose; + end + + def deanimate + @bitmap.deanimate; + end + + def copy + @bitmap.copy; + end + + def scale_bitmap(scale) + return if scale == 1 + new_width = @bitmap.bitmap.width * scale + new_height = @bitmap.bitmap.height * scale + + destination_rect = Rect.new(0, 0, new_width, new_height) + source_rect = Rect.new(0, 0, @bitmap.bitmap.width, @bitmap.bitmap.height) + new_bitmap = Bitmap.new(new_width, new_height) + new_bitmap.stretch_blt( + destination_rect, + @bitmap.bitmap, + source_rect + ) + @bitmap.bitmap = new_bitmap + end + + # def mirror + # for x in 0..@bitmap.bitmap.width / 2 + # for y in 0..@bitmap.bitmap.height - 2 + # temp = @bitmap.bitmap.get_pixel(x, y) + # newPix = @bitmap.bitmap.get_pixel((@bitmap.bitmap.width - x), y) + # + # @bitmap.bitmap.set_pixel(x, y, newPix) + # @bitmap.bitmap.set_pixel((@bitmap.bitmap.width - x), y, temp) + # end + # end + # end + + def mirror + @bitmap.bitmap + end + +end + + +#=============================================================================== +# +#=============================================================================== +class PngAnimatedBitmap + attr_accessor :frames + + # Creates an animated bitmap from a PNG file. + def initialize(dir, filename, hue = 0) + @frames = [] + @currentFrame = 0 + @framecount = 0 + panorama = RPG::Cache.load_bitmap(dir, filename, hue) + if filename[/^\[(\d+)(?:,(\d+))?\]/] # Starts with 1 or 2 numbers in brackets + # File has a frame count + numFrames = $1.to_i + delay = $2.to_i + delay = 10 if delay == 0 + raise "Invalid frame count in #{filename}" if numFrames <= 0 + raise "Invalid frame delay in #{filename}" if delay <= 0 + if panorama.width % numFrames != 0 + raise "Bitmap's width (#{panorama.width}) is not divisible by frame count: #{filename}" + end + @frameDelay = delay + subWidth = panorama.width / numFrames + for i in 0...numFrames + subBitmap = BitmapWrapper.new(subWidth, panorama.height) + subBitmap.blt(0, 0, panorama, Rect.new(subWidth * i, 0, subWidth, panorama.height)) + @frames.push(subBitmap) + end + panorama.dispose + else + @frames = [panorama] + end + end + + def [](index) + return @frames[index] + end + + def width + self.bitmap.width; + end + + def height + self.bitmap.height; + end + + def deanimate + for i in 1...@frames.length + @frames[i].dispose + end + @frames = [@frames[0]] + @currentFrame = 0 + return @frames[0] + end + + def bitmap + return @frames[@currentFrame] + end + + def currentIndex + return @currentFrame + end + + def frameDelay(_index) + return @frameDelay + end + + def length + return @frames.length + end + + def each + @frames.each { |item| yield item } + end + + def totalFrames + return @frameDelay * @frames.length + end + + def disposed? + return @disposed + end + + def update + return if disposed? + if @frames.length > 1 + @framecount += 1 + if @framecount >= @frameDelay + @framecount = 0 + @currentFrame += 1 + @currentFrame %= @frames.length + end + end + end + + def dispose + if !@disposed + @frames.each { |f| f.dispose } + end + @disposed = true + end + + def copy + x = self.clone + x.frames = x.frames.clone + for i in 0...x.frames.length + x.frames[i] = x.frames[i].copy + end + return x + end +end + +#=============================================================================== +# +#=============================================================================== +class GifBitmap + attr_accessor :bitmap + attr_reader :loaded_from_cache + # Creates a bitmap from a GIF file. Can also load non-animated bitmaps. + def initialize(dir, filename, hue = 0) + @bitmap = nil + @disposed = false + @loaded_from_cache = false + filename = "" if !filename + begin + @bitmap = RPG::Cache.load_bitmap(dir, filename, hue) + @loaded_from_cache = true + rescue + @bitmap = nil + end + @bitmap = BitmapWrapper.new(32, 32) if @bitmap.nil? + @bitmap.play if @bitmap&.animated? + end + + def [](_index) + return @bitmap + end + + def deanimate + @bitmap&.goto_and_stop(0) if @bitmap&.animated? + return @bitmap + end + + def currentIndex + return @bitmap&.current_frame || 0 + end + + def length + return @bitmap&.frame_count || 1 + end + + def each + yield @bitmap + end + + def totalFrames + f_rate = @bitmap.frame_rate + f_rate = 1 if f_rate.nil? || f_rate == 0 + return (@bitmap) ? (@bitmap.frame_count / f_rate).floor : 1 + end + + def disposed? + return @disposed + end + + def width + return @bitmap&.width || 0 + end + + def height + return @bitmap&.height || 0 + end + + # Gifs are animated automatically by mkxp-z. This function does nothing. + def update; end + + def dispose + return if @disposed + @bitmap.dispose + @disposed = true + end + + def copy + x = self.clone + x.bitmap = @bitmap.copy if @bitmap + return x + end +end + +#=============================================================================== +# +#=============================================================================== +def pbGetTileBitmap(filename, tile_id, hue, width = 1, height = 1) + return RPG::Cache.tileEx(filename, tile_id, hue, width, height) { |f| + AnimatedBitmap.new("Graphics/Tilesets/" + filename).deanimate + } +end + +def pbGetTileset(name, hue = 0) + return AnimatedBitmap.new("Graphics/Tilesets/" + name, hue).deanimate +end + +def pbGetAutotile(name, hue = 0) + return AnimatedBitmap.new("Graphics/Autotiles/" + name, hue).deanimate +end + +def pbGetAnimation(name, hue = 0) + return AnimatedBitmap.new("Graphics/Animations/" + name, hue).deanimate +end diff --git a/Data/Scripts/007_Objects and windows/009_Planes.rb b/Data/Scripts/007_Objects and windows/009_Planes.rb new file mode 100644 index 000000000..26258bc24 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/009_Planes.rb @@ -0,0 +1,230 @@ +#=============================================================================== +# +#=============================================================================== +class Plane + def update; end + def refresh; end +end + + + +#=============================================================================== +# This class works around a limitation that planes are always +# 640 by 480 pixels in size regardless of the window's size. +#=============================================================================== +class LargePlane < Plane + attr_accessor :borderX + attr_accessor :borderY + + def initialize(viewport=nil) + @__sprite=Sprite.new(viewport) + @__disposed=false + @__ox=0 + @__oy=0 + @__bitmap=nil + @__visible=true + @__sprite.visible=false + @borderX=0 + @borderY=0 + end + + def disposed? + return @__disposed + end + + def dispose + if !@__disposed + @__sprite.bitmap.dispose if @__sprite.bitmap + @__sprite.dispose + @__sprite=nil + @__bitmap=nil + @__disposed=true + end + #super + end + + def ox; @__ox; end + def oy; @__oy; end + + def ox=(value); + return if @__ox==value + @__ox = value + refresh + end + + def oy=(value); + return if @__oy==value + @__oy = value + refresh + end + + def bitmap + return @__bitmap + end + + def bitmap=(value) + if value==nil + if @__bitmap!=nil + @__bitmap=nil + @__sprite.visible=(@__visible && !@__bitmap.nil?) + end + elsif @__bitmap!=value && !value.disposed? + @__bitmap=value + refresh + elsif value.disposed? + if @__bitmap!=nil + @__bitmap=nil + @__sprite.visible=(@__visible && !@__bitmap.nil?) + end + end + end + + def viewport; @__sprite.viewport; end + def zoom_x; @__sprite.zoom_x; end + def zoom_y; @__sprite.zoom_y; end + def opacity; @__sprite.opacity; end + def blend_type; @__sprite.blend_type; end + def visible; @__visible; end + def z; @__sprite.z; end + def color; @__sprite.color; end + def tone; @__sprite.tone; end + + def zoom_x=(v); + return if @__sprite.zoom_x==v + @__sprite.zoom_x = v + refresh + end + + def zoom_y=(v); + return if @__sprite.zoom_y==v + @__sprite.zoom_y = v + refresh + end + + def opacity=(v); @__sprite.opacity=(v); end + def blend_type=(v); @__sprite.blend_type=(v); end + def visible=(v); @__visible=v; @__sprite.visible=(@__visible && !@__bitmap.nil?); end + def z=(v); @__sprite.z=(v); end + def color=(v); @__sprite.color=(v); end + def tone=(v); @__sprite.tone=(v); end + def update; ;end + + def refresh + @__sprite.visible = (@__visible && !@__bitmap.nil?) + if @__bitmap + if !@__bitmap.disposed? + @__ox += @__bitmap.width*@__sprite.zoom_x if @__ox<0 + @__oy += @__bitmap.height*@__sprite.zoom_y if @__oy<0 + @__ox -= @__bitmap.width*@__sprite.zoom_x if @__ox>@__bitmap.width + @__oy -= @__bitmap.height*@__sprite.zoom_y if @__oy>@__bitmap.height + dwidth = (Graphics.width/@__sprite.zoom_x+@borderX).to_i # +2 + dheight = (Graphics.height/@__sprite.zoom_y+@borderY).to_i # +2 + @__sprite.bitmap = ensureBitmap(@__sprite.bitmap,dwidth,dheight) + @__sprite.bitmap.clear + tileBitmap(@__sprite.bitmap,@__bitmap,@__bitmap.rect) + else + @__sprite.visible = false + end + end + end + + private + + def ensureBitmap(bitmap,dwidth,dheight) + if !bitmap || bitmap.disposed? || bitmap.width0; left -= srcbitmap.width; end + while top>0; top -= srcbitmap.height; end + y = top + while y",ret) +end + +def shadowctag(base,shadow) + return sprintf("",colorToRgb16(base),colorToRgb16(shadow)) +end + +def shadowc3tag(base,shadow) + return sprintf("",colorToRgb32(base),colorToRgb32(shadow)) +end + +def shadowctagFromColor(color) + return shadowc3tag(color,getContrastColor(color)) +end + +def shadowctagFromRgb(param) + return shadowctagFromColor(rgbToColor(param)) +end + +def colorToRgb32(color) + return "" if !color + if color.alpha.to_i==255 + return sprintf("%02X%02X%02X",color.red.to_i,color.green.to_i,color.blue.to_i) + else + return sprintf("%02X%02X%02X%02X", + color.red.to_i,color.green.to_i,color.blue.to_i,color.alpha.to_i) + end +end + +def colorToRgb16(color) + ret=(color.red.to_i>>3) + ret|=((color.green.to_i>>3)<<5) + ret|=((color.blue.to_i>>3)<<10) + return sprintf("%04X",ret) +end + +def rgbToColor(param) + return Font.default_color if !param + baseint=param.to_i(16) + if param.length==8 # 32-bit hex + return Color.new( + (baseint>>24)&0xFF, + (baseint>>16)&0xFF, + (baseint>>8)&0xFF, + (baseint)&0xFF + ) + elsif param.length==6 # 24-bit hex + return Color.new( + (baseint>>16)&0xFF, + (baseint>>8)&0xFF, + (baseint)&0xFF + ) + elsif param.length==4 # 16-bit hex + return Color.new( + ((baseint)&0x1F)<<3, + ((baseint>>5)&0x1F)<<3, + ((baseint>>10)&0x1F)<<3 + ) + elsif param.length==1 # Color number + i=param.to_i + return Font.default_color if i>=8 + return [ + Color.new(255, 255, 255, 255), + Color.new(128, 128, 255, 255), + Color.new(255, 128, 128, 255), + Color.new(128, 255, 128, 255), + Color.new(128, 255, 255, 255), + Color.new(255, 128, 255, 255), + Color.new(255, 255, 128, 255), + Color.new(192, 192, 192, 255) + ][i] + else + return Font.default_color + end +end + +def Rgb16ToColor(param) + baseint=param.to_i(16) + return Color.new( + ((baseint)&0x1F)<<3, + ((baseint>>5)&0x1F)<<3, + ((baseint>>10)&0x1F)<<3 + ) +end + +def getContrastColor(color) + raise "No color given" if !color + r=color.red; g=color.green; b=color.blue + yuv=[ + r * 0.299 + g * 0.587 + b * 0.114, + r * -0.1687 + g * -0.3313 + b * 0.500 + 0.5, + r * 0.500 + g * -0.4187 + b * -0.0813 + 0.5 + ] + if yuv[0]<127.5 + yuv[0]+=(255-yuv[0])/2 + else + yuv[0]=yuv[0]/2 + end + return Color.new( + yuv[0] + 1.4075 * (yuv[2] - 0.5), + yuv[0] - 0.3455 * (yuv[1] - 0.5) - 0.7169 * (yuv[2] - 0.5), + yuv[0] + 1.7790 * (yuv[1] - 0.5), + color.alpha + ) +end + + + +#=============================================================================== +# Format text +#=============================================================================== +FORMATREGEXP = /<(\/?)(c|c2|c3|o|fn|br|fs|i|b|r|pg|pog|u|s|icon|img|ac|ar|al|outln|outln2)(\s*\=\s*([^>]*))?>/i + +def fmtescape(text) + if text[/[&<>]/] + text2=text.gsub(/&/,"&") + text2.gsub!(//,">") + return text2 + end + return text +end + +def toUnformattedText(text) + text2=text.gsub(FORMATREGEXP,"") + text2.gsub!(/</,"<") + text2.gsub!(/>/,">") + text2.gsub!(/'/,"'") + text2.gsub!(/"/,"\"") + text2.gsub!(/&/,"&") + return text2 +end + +def unformattedTextLength(text) + return toUnformattedText(text).scan(/./m).length +end + +def itemIconTag(item) + return "" if !item + if item.respond_to?("icon_name") + return sprintf("",item.icon_name) + else + ix=item.icon_index % 16 * 24 + iy=item.icon_index / 16 * 24 + return sprintf("",ix,iy) + end +end + +def getFormattedTextForDims(bitmap,xDst,yDst,widthDst,heightDst,text,lineheight, + newlineBreaks=true,explicitBreaksOnly=false) + text2=text.gsub(/<(\/?)(c|c2|c3|o|u|s)(\s*\=\s*([^>]*))?>/i,"") + if newlineBreaks + text2.gsub!(/<(\/?)(br)(\s*\=\s*([^>]*))?>/i,"\n") + end + return getFormattedText( + bitmap,xDst,yDst,widthDst,heightDst, + text2,lineheight,newlineBreaks, + explicitBreaksOnly,true) +end + +def getFormattedTextFast(bitmap,xDst,yDst,widthDst,heightDst,text,lineheight, + newlineBreaks=true,explicitBreaksOnly=false) + x=y=0 + characters=[] + textchunks=[] + textchunks.push(text) + text=textchunks.join("") + textchars=text.scan(/./m) + lastword=[0,0] # position of last word + hadspace=false + hadnonspace=false + bold=bitmap.font.bold + italic=bitmap.font.italic + colorclone=bitmap.font.color + defaultfontname=bitmap.font.name + if defaultfontname.is_a?(Array) + defaultfontname=defaultfontname.find { |i| Font.exist?(i) } || "Arial" + elsif !Font.exist?(defaultfontname) + defaultfontname="Arial" + end + defaultfontname=defaultfontname.clone + havenl=false + position=0 + while positionwidthDst && lastword[1]!=0 && + (!hadnonspace || !hadspace) + havenl=true + characters.insert(lastword[0],["\n",x,y*lineheight+yDst,0,lineheight, + false,false,false,colorclone,nil,false,false,"",8,position]) + lastword[0]+=1 + y+=1 + x=0 + for i in lastword[0]...characters.length + characters[i][2]+=lineheight + charwidth=characters[i][3]-2 + characters[i][1]=x + x+=charwidth + end + lastword[1]=0 + end + position+=1 + end + # This code looks at whether the text occupies exactly two lines when + # displayed. If it does, it balances the length of each line. +=begin + # Count total number of lines + numlines = (x==0 && y>0) ? y-1 : y + realtext = (newlineBreaks) ? text : text.gsub(/\n/," ") + if numlines==2 && !explicitBreaksOnly && !realtext[/\n/] && realtext.length>=50 + # Set half to middle of text (known to contain no formatting) + half = realtext.length/2 + leftSearch = 0 + rightSearch = 0 + # Search left for a space + i = half; while i>=0 + break if realtext[i,1][/\s/]||isWaitChar(realtext[i]) # found a space + leftSearch += 1 + i -= 1 + end + # Search right for a space + i = half; while i=0 + for j in firstspace...i + characters[j]=nil + end + firstspace=-1 + elsif characters[i][0][/[ \r\t]/] + if firstspace<0 + firstspace=i + end + else + firstspace=-1 + end + end + if firstspace>0 + for j in firstspace...characters.length + characters[j]=nil + end + end + characters.compact! + end + for i in 0...characters.length + characters[i][1]=xDst+characters[i][1] + end + # Remove all characters with Y greater or equal to _yDst_+_heightDst_ + if heightDst>=0 + for i in 0...characters.length + if characters[i][2]>=yDst+heightDst + characters[i]=nil + end + end + characters.compact! + end + return characters +end + +def isWaitChar(x) + return (x=="\001" || x=="\002") +end + +def getLastParam(array,default) + i=array.length-1 + while i>=0 + return array[i] if array[i] + i-=1 + end + return default +end + +def getLastColors(colorstack,opacitystack,defaultcolors) + colors=getLastParam(colorstack,defaultcolors) + opacity=getLastParam(opacitystack,255) + if opacity!=255 + colors=[Color.new(colors[0].red,colors[0].green,colors[0].blue, + colors[0].alpha*opacity/255), + colors[1] ? Color.new(colors[1].red,colors[1].green,colors[1].blue, + colors[1].alpha*opacity/255) : nil] + end + return colors +end + + + +#=============================================================================== +# Formats a string of text and returns an array containing a list of formatted +# characters. +#=============================================================================== +=begin +Parameters: +bitmap: Source bitmap. Will be used to determine the default font of + the text. +xDst: X coordinate of the text's top left corner. +yDst: Y coordinate of the text's top left corner. +widthDst: Width of the text. Used to determine line breaks. +heightDst: Height of the text. If -1, there is no height restriction. If + 1 or greater, any characters exceeding the height are removed + from the returned list. +newLineBreaks: If true, newline characters will be treated as line breaks. The + default is true. + +Return Values: +A list of formatted characters. Returns an empty array if _bitmap_ is nil +or disposed, or if _widthDst_ is 0 or less or _heightDst_ is 0. + +Formatting Specification: +This function uses the following syntax when formatting the text. + ... - Formats the text in bold. + ... - Formats the text in italics. + ... - Underlines the text. + ... - Draws a strikeout line over the text. + ... - Left-aligns the text. Causes line breaks before and after + the text. + - Right-aligns the text until the next line break. + ... - Right-aligns the text. Causes line breaks before and after + the text. + ... - Centers the text. Causes line breaks before and after the + text. +
- Causes a line break. + ... - Color specification. A total of four formats are supported: + RRGGBBAA, RRGGBB, 16-bit RGB, and Window_Base color numbers. + ... - Color specification where the first half is the base color + and the second half is the shadow color. 16-bit RGB is + supported. +Added 2009-10-20 + ... - Color specification where B is the base color and S is the + shadow color. B and/or S can be omitted. A total of four + formats are supported: + RRGGBBAA, RRGGBB, 16-bit RGB, and Window_Base color numbers. +Added 2009-9-12 + - Displays the text in the given opacity (0-255) +Added 2009-10-19 + - Displays the text in outline format. +Added 2010-05-12 + - Displays the text in outline format (outlines more + exaggerated. + ... - Formats the text in the specified font, or Arial if the + font doesn't exist. + ... - Changes the font size to X. + - Displays the icon X (in Graphics/Icons/). + +In addition, the syntax supports the following: +' - Converted to "'". +< - Converted to "<". +> - Converted to ">". +& - Converted to "&". +" - Converted to double quotation mark. + +To draw the characters, pass the returned array to the +_drawFormattedChars_ function. +=end + +def getFormattedText(bitmap,xDst,yDst,widthDst,heightDst,text,lineheight=32, + newlineBreaks=true,explicitBreaksOnly=false, + collapseAlignments=false) + dummybitmap=nil + if !bitmap || bitmap.disposed? # allows function to be called with nil bitmap + dummybitmap=Bitmap.new(1,1) + bitmap=dummybitmap + return + end + if !bitmap || bitmap.disposed? || widthDst<=0 || heightDst==0 || text.length==0 + return [] + end + textchunks=[] + controls=[] + oldtext=text + while text[FORMATREGEXP] + textchunks.push($~.pre_match) + if $~[3] + controls.push([$~[2].downcase,$~[4],-1,$~[1]=="/" ? true : false]) + else + controls.push([$~[2].downcase,"",-1,$~[1]=="/" ? true : false]) + end + text=$~.post_match + end + if controls.length==0 + ret=getFormattedTextFast(bitmap,xDst,yDst,widthDst,heightDst,text,lineheight, + newlineBreaks,explicitBreaksOnly) + dummybitmap.dispose if dummybitmap + return ret + end + x=y=0 + characters=[] + charactersInternal=[] + realtext=nil + realtextStart="" + if !explicitBreaksOnly && textchunks.join("").length==0 + # All commands occurred at the beginning of the text string + realtext=(newlineBreaks) ? text : text.gsub(/\n/," ") + realtextStart=oldtext[0,oldtext.length-realtext.length] + realtextHalf=text.length/2 + end + textchunks.push(text) + for chunk in textchunks + chunk.gsub!(/</,"<") + chunk.gsub!(/>/,">") + chunk.gsub!(/'/,"'") + chunk.gsub!(/"/,"\"") + chunk.gsub!(/&/,"&") + end + textlen=0 + for i in 0...controls.length + textlen+=textchunks[i].scan(/./m).length + controls[i][2]=textlen + end + text=textchunks.join("") + textchars=text.scan(/./m) + colorstack=[] + boldcount=0 + italiccount=0 + outlinecount=0 + underlinecount=0 + strikecount=0 + rightalign=0 + outline2count=0 + opacitystack=[] + oldfont=bitmap.font.clone + defaultfontname=bitmap.font.name + defaultfontsize=bitmap.font.size + fontsize=defaultfontsize + fontnamestack=[] + fontsizestack=[] + defaultcolors=[oldfont.color.clone,nil] + if defaultfontname.is_a?(Array) + defaultfontname=defaultfontname.find { |i| Font.exist?(i) } || "Arial" + elsif !Font.exist?(defaultfontname) + defaultfontname="Arial" + end + defaultfontname=defaultfontname.clone + fontname=defaultfontname + alignstack=[] + lastword=[0,0] # position of last word + hadspace=false + hadnonspace=false + havenl=false + position=0 + while position0 && nextline==0 + else + alignstack.pop + nextline=1 if x>0 && nextline==0 + end + elsif control=="al" # Left align + if !endtag + alignstack.push(0) + nextline=1 if x>0 && nextline==0 + else + alignstack.pop + nextline=1 if x>0 && nextline==0 + end + elsif control=="ac" # Center align + if !endtag + alignstack.push(2) + nextline=1 if x>0 && nextline==0 + else + alignstack.pop + nextline=1 if x>0 && nextline==0 + end + elsif control=="icon" # Icon + if !endtag + param=param.sub(/\s+$/,"") + graphic="Graphics/Icons/#{param}" + controls[i]=nil + break + end + elsif control=="img" # Icon + if !endtag + param=param.sub(/\s+$/,"") + param=param.split("|") + graphic=param[0] + if param.length>1 + graphicX=param[1].to_i + graphicY=param[2].to_i + graphicWidth=param[3].to_i + graphicHeight=param[4].to_i + end + controls[i]=nil + break + end + elsif control=="br" # Line break + if !endtag + nextline+=1 + end + elsif control=="r" # Right align this line + if !endtag + x=0 + rightalign=1; lastword=[characters.length,x] + end + end + controls[i]=nil + end + end + bitmap.font.bold=(boldcount>0) + bitmap.font.italic=(italiccount>0) + if graphic + if !graphicWidth + tempgraphic=Bitmap.new(graphic) + graphicWidth=tempgraphic.width + graphicHeight=tempgraphic.height + tempgraphic.dispose + end + width=graphicWidth # +8 # No padding + xStart=0 # 4 + yStart=[(lineheight/2)-(graphicHeight/2),0].max + graphicRect=Rect.new(graphicX,graphicY,graphicWidth,graphicHeight) + else + xStart=0 + yStart=0 + width=isWaitChar(textchars[position]) ? 0 : bitmap.text_size(textchars[position]).width + width+=2 if width>0 && outline2count>0 + end + if rightalign==1 && nextline==0 + alignment=1 + else + alignment=getLastParam(alignstack,0) + end + nextline.times do + havenl=true + characters.push(["\n",x,y*lineheight+yDst,0,lineheight,false,false,false, + defaultcolors[0],defaultcolors[1],false,false,"",8,position,nil,0]) + charactersInternal.push([alignment,y,0]) + y+=1 + x=0 + rightalign=0 + lastword=[characters.length,x] + hadspace=false + hadnonspace=false + end + if textchars[position]=="\n" + if newlineBreaks + if nextline==0 + havenl=true + characters.push(["\n",x,y*lineheight+yDst,0,lineheight,false,false,false, + defaultcolors[0],defaultcolors[1],false,false,"",8,position,nil,0]) + charactersInternal.push([alignment,y,0]) + y+=1 + x=0 + end + rightalign=0 + hadspace=true + hadnonspace=false + position+=1 + next + else + textchars[position]=" " + if !graphic + width=bitmap.text_size(textchars[position]).width + width+=2 if width>0 && outline2count>0 + end + end + end + isspace=(textchars[position][/\s/] || isWaitChar(textchars[position])) ? true : false + if hadspace && !isspace + # set last word to here + lastword[0]=characters.length + lastword[1]=x + hadspace=false + hadnonspace=true + elsif isspace + hadspace=true + end + texty=(lineheight*y)+yDst+yStart + colors=getLastColors(colorstack,opacitystack,defaultcolors) + # Push character + if heightDst<0 || texty0) ? 2+(width/2) : 2 + characters.push([ + graphic ? graphic : textchars[position], + x+xStart,texty,width+extraspace,lineheight, + graphic ? true : false, + (boldcount>0),(italiccount>0),colors[0],colors[1], + (underlinecount>0),(strikecount>0),fontname,fontsize, + position,graphicRect, + ((outlinecount>0) ? 1 : 0)+((outline2count>0) ? 2 : 0) + ]) + charactersInternal.push([alignment,y,xStart,textchars[position],extraspace]) + end + x+=width + if !explicitBreaksOnly && x+2>widthDst && lastword[1]!=0 && + (!hadnonspace || !hadspace) + havenl=true + characters.insert(lastword[0],["\n",x,y*lineheight+yDst,0,lineheight,false, + false,false,defaultcolors[0],defaultcolors[1],false,false,"",8,position, + nil]) + charactersInternal.insert(lastword[0],[alignment,y,0]) + lastword[0]+=1 + y+=1 + x=0 + for i in lastword[0]...characters.length + characters[i][2]+=lineheight + charactersInternal[i][1]+=1 + extraspace=(charactersInternal[i][4]) ? charactersInternal[i][4] : 0 + charwidth=characters[i][3]-extraspace + characters[i][1]=x+charactersInternal[i][2] + x+=charwidth + end + lastword[1]=0 + end + position+=1 if !graphic + end + # This code looks at whether the text occupies exactly two lines when + # displayed. If it does, it balances the length of each line. +=begin + # Count total number of lines + numlines = (x==0 && y>0) ? y : y+1 + if numlines==2 && realtext && !realtext[/\n/] && realtext.length>=50 + # Set half to middle of text (known to contain no formatting) + half = realtext.length/2 + leftSearch = 0 + rightSearch = 0 + # Search left for a space + i = half; while i>=0 + break if realtext[i,1][/\s/]||isWaitChar(realtext[i,1]) # found a space + leftSearch += 1 + i -= 1 + end + # Search right for a space + i = half; while i=0 + for j in firstspace...i + characters[j]=nil + charactersInternal[j]=nil + end + firstspace=-1 + elsif characters[i][0][/[ \r\t]/] + if firstspace<0 + firstspace=i + end + else + firstspace=-1 + end + end + if firstspace>0 + for j in firstspace...characters.length + characters[j]=nil + charactersInternal[j]=nil + end + end + characters.compact! + charactersInternal.compact! + end + # Calculate Xs based on alignment + # First, find all text runs with the same alignment on the same line + totalwidth=0 + widthblocks=[] + lastalign=0 + lasty=0 + runstart=0 + for i in 0...characters.length + c=characters[i] + if i>0 && (charactersInternal[i][0]!=lastalign || + charactersInternal[i][1]!=lasty) + # Found end of run + widthblocks.push([runstart,i,lastalign,totalwidth,lasty]) + runstart=i + totalwidth=0 + end + lastalign=charactersInternal[i][0] + lasty=charactersInternal[i][1] + extraspace=(charactersInternal[i][4]) ? charactersInternal[i][4] : 0 + totalwidth+=c[3]-extraspace + end + widthblocks.push([runstart,characters.length,lastalign,totalwidth,lasty]) + if collapseAlignments + # Calculate the total width of each line + totalLineWidths=[] + for block in widthblocks + y=block[4] + if !totalLineWidths[y] + totalLineWidths[y]=0 + end + if totalLineWidths[y]!=0 + # padding in case more than one line has different alignments + totalLineWidths[y]+=16 + end + totalLineWidths[y]+=block[3] + end + # Calculate a new width for the next step + widthDst=[widthDst,(totalLineWidths.compact.max || 0)].min + end + # Now, based on the text runs found, recalculate Xs + for block in widthblocks + next if block[0]>=block[1] + for i in block[0]...block[1] + case block[2] + when 1 then characters[i][1] = xDst + (widthDst - block[3] - 4) + characters[i][1] + when 2 then characters[i][1] = xDst + ((widthDst / 2) - (block[3] / 2)) + characters[i][1] + else characters[i][1] = xDst + characters[i][1] + end + end + end + # Remove all characters with Y greater or equal to _yDst_+_heightDst_ + if heightDst>=0 + for i in 0...characters.length + if characters[i][2]>=yDst+heightDst + characters[i]=nil + end + end + characters.compact! + end + bitmap.font=oldfont + dummybitmap.dispose if dummybitmap + return characters +end + + + +#=============================================================================== +# Draw text and images on a bitmap +#=============================================================================== +def getLineBrokenText(bitmap,value,width,dims) + x=0 + y=0 + textheight=0 + ret=[] + if dims + dims[0]=0 + dims[1]=0 + end + line=0 + position=0 + column=0 + return ret if !bitmap || bitmap.disposed? || width<=0 + textmsg=value.clone + ret.push(["",0,0,0,bitmap.text_size("X").height,0,0,0,0]) + while ((c = textmsg.slice!(/\n|(\S*([ \r\t\f]?))/)) != nil) + break if c=="" + length=c.scan(/./m).length + ccheck=c + if ccheck=="\n" + ret.push(["\n",x,y,0,textheight,line,position,column,0]) + x=0 + y+=(textheight==0) ? bitmap.text_size("X").height : textheight + line+=1 + textheight=0 + column=0 + position+=length + ret.push(["",x,y,0,textheight,line,position,column,0]) + next + end + words=[ccheck] + for i in 0...words.length + word=words[i] + if word && word!="" + textSize=bitmap.text_size(word) + textwidth=textSize.width + if x>0 && x+textwidth>=width-2 + # Zero-length word break + ret.push(["",x,y,0,textheight,line,position,column,0]) + x=0 + column=0 + y+=(textheight==0) ? bitmap.text_size("X").height : textheight + line+=1 + textheight=0 + end + textheight=[textheight,textSize.height].max + ret.push([word,x,y,textwidth,textheight,line,position,column,length]) + x+=textwidth + dims[0]=x if dims && dims[0]]+)>/ + reNoMatch=/]+>/ + return ret if !bitmap || bitmap.disposed? || width<=0 + textmsg=value.clone + color=Font.default_color + while (c = textmsg.slice!(/\n|[^ \r\t\f\n\-]*\-+|(\S*([ \r\t\f]?))/)) != nil + break if c=="" + ccheck=c + if ccheck=="\n" + x=0 + y+=32 + next + end + if ccheck[/0 && x+textwidth>width + minTextSize=bitmap.text_size(word.gsub(/\s*/,"")) + if x>0 && x+minTextSize.width>width + x=0 + y+=32 + end + end + ret.push([word,x,y,textwidth,32,color]) + x+=textwidth + dims[0]=x if dims && dims[0]"+text + chars=getFormattedText(bitmap,x,y,width,-1,text,lineheight) + drawFormattedChars(bitmap,chars) +end + +# Unused +def pbDrawShadow(bitmap,x,y,width,height,string) + return if !bitmap || !string + pbDrawShadowText(bitmap,x,y,width,height,string,nil,bitmap.font.color) +end + +def pbDrawShadowText(bitmap,x,y,width,height,string,baseColor,shadowColor=nil,align=0) + return if !bitmap || !string + width=(width<0) ? bitmap.text_size(string).width+1 : width + height=(height<0) ? bitmap.text_size(string).height+1 : height + y += 4 + if shadowColor && shadowColor.alpha>0 + bitmap.font.color=shadowColor + bitmap.draw_text(x+2,y,width,height,string,align) + bitmap.draw_text(x,y+2,width,height,string,align) + bitmap.draw_text(x+2,y+2,width,height,string,align) + end + if baseColor && baseColor.alpha>0 + bitmap.font.color=baseColor + bitmap.draw_text(x,y,width,height,string,align) + end +end + +def pbDrawOutlineText(bitmap,x,y,width,height,string,baseColor,shadowColor=nil,align=0) + return if !bitmap || !string + width=(width<0) ? bitmap.text_size(string).width+4 : width + height=(height<0) ? bitmap.text_size(string).height+4 : height + if shadowColor && shadowColor.alpha>0 + bitmap.font.color=shadowColor + bitmap.draw_text(x-2,y-2,width,height,string,align) + bitmap.draw_text(x,y-2,width,height,string,align) + bitmap.draw_text(x+2,y-2,width,height,string,align) + bitmap.draw_text(x-2,y,width,height,string,align) + bitmap.draw_text(x+2,y,width,height,string,align) + bitmap.draw_text(x-2,y+2,width,height,string,align) + bitmap.draw_text(x,y+2,width,height,string,align) + bitmap.draw_text(x+2,y+2,width,height,string,align) + end + if baseColor && baseColor.alpha>0 + bitmap.font.color=baseColor + bitmap.draw_text(x,y,width,height,string,align) + end +end + +# Draws text on a bitmap. _textpos_ is an array of text commands. Each text +# command is an array that contains the following: +# 0 - Text to draw +# 1 - X coordinate +# 2 - Y coordinate +# 3 - If true or 1, the text is right aligned. If 2, the text is centered. +# Otherwise, the text is left aligned. +# 4 - Base color +# 5 - Shadow color +# 6 - If true or 1, the text has an outline. Otherwise, the text has a shadow. +def pbDrawTextPositions(bitmap,textpos) + for i in textpos + textsize = bitmap.text_size(i[0]) + + x = i[1] + y = i[2] + 6 + if i[3]==true || i[3]==1 # right align + x -= textsize.width + elsif i[3]==2 # centered + x -= (textsize.width/2) + end + if i[6]==true || i[6]==1 # outline text + pbDrawOutlineText(bitmap,x,y,textsize.width,textsize.height,i[0],i[4],i[5]) + else + pbDrawShadowText(bitmap,x,y,textsize.width,textsize.height,i[0],i[4],i[5]) + end + end +end + + + +#=============================================================================== +# Draw images on a bitmap +#=============================================================================== +def pbCopyBitmap(dstbm,srcbm,x,y,opacity=255) + rc = Rect.new(0,0,srcbm.width,srcbm.height) + dstbm.blt(x,y,srcbm,rc,opacity) +end + +def pbDrawImagePositions(bitmap,textpos) + for i in textpos + srcbitmap=AnimatedBitmap.new(pbBitmapName(i[0])) + x=i[1] + y=i[2] + srcx=i[3] || 0 + srcy=i[4] || 0 + width=(i[5] && i[5]>=0) ? i[5] : srcbitmap.width + height=(i[6] && i[6]>=0) ? i[6] : srcbitmap.height + color = i[7] || nil + if color + srcbitmap.pbSetColorValue(color) + end + srcrect=Rect.new(srcx,srcy,width,height) + bitmap.blt(x,y,srcbitmap.bitmap,srcrect) + + + srcbitmap.dispose + end +end + + +#added for quest log script ( edit ) +def renderMultiLine(bitmap,xDst,yDst,normtext,maxheight,baseColor,shadowColor) + for i in 0...normtext.length + width=normtext[i][3] + textx=normtext[i][1]+xDst + texty=normtext[i][2]+yDst + if shadowColor + height=normtext[i][4] + text=normtext[i][0] + bitmap.font.color=shadowColor + bitmap.draw_text(textx-2,texty-2,width,height,text,0) + bitmap.draw_text(textx,texty-2,width,height,text,0) + bitmap.draw_text(textx+2,texty-2,width,height,text,0) + bitmap.draw_text(textx-2,texty,width,height,text,0) + bitmap.draw_text(textx+2,texty,width,height,text,0) + bitmap.draw_text(textx-2,texty+2,width,height,text,0) + bitmap.draw_text(textx,texty+2,width,height,text,0) + bitmap.draw_text(textx+2,texty+2,width,height,text,0) + end + if baseColor + height=normtext[i][4] + text=normtext[i][0] + bitmap.font.color=baseColor + bitmap.draw_text(textx,texty,width,height,text,0) + end + end +end + +def drawTextExMulti(bitmap,x,y,width,numlines,text,baseColor,shadowColor) + normtext=getLineBrokenChunks(bitmap,text,width,nil,true) + renderMultiLine(bitmap,x,y,normtext,numlines*32,baseColor,shadowColor) +end \ No newline at end of file diff --git a/Data/Scripts/007_Objects and windows/011_Messages.rb b/Data/Scripts/007_Objects and windows/011_Messages.rb new file mode 100644 index 000000000..99b90a966 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/011_Messages.rb @@ -0,0 +1,1057 @@ +#=============================================================================== +# +#=============================================================================== +class Scene_Map + def updatemini + oldmws = $game_temp.message_window_showing + $game_temp.message_window_showing = true + loop do + $game_map.update + $game_player.update + $game_system.update + if $game_screen + $game_screen.update + else + $game_map.screen.update + end + break unless $game_temp.player_transferring + transfer_player + break if $game_temp.transition_processing + end + $game_temp.message_window_showing = oldmws + @spriteset.update if @spriteset + @message_window.update if @message_window + end +end + +class Scene_Battle + def updatemini + if self.respond_to?("update_basic") + update_basic(true) + update_info_viewport # Update information viewport + else + oldmws = $game_temp.message_window_showing + $game_temp.message_window_showing = true + # Update system (timer) and screen + $game_system.update + if $game_screen + $game_screen.update + else + $game_map.screen.update + end + # If timer has reached 0 + if $game_system.timer_working && $game_system.timer == 0 + # Abort battle + $game_temp.battle_abort = true + end + # Update windows + @help_window.update if @help_window + @party_command_window.update if @party_command_window + @actor_command_window.update if @actor_command_window + @status_window.update if @status_window + $game_temp.message_window_showing = oldmws + @message_window.update if @message_window + # Update sprite set + @spriteset.update if @spriteset + end + end +end + +def pbMapInterpreter + if $game_map.respond_to?("interpreter") + return $game_map.interpreter + elsif $game_system + return $game_system.map_interpreter + end + return nil +end + +def pbMapInterpreterRunning? + interp = pbMapInterpreter + return interp && interp.running? +end + +def pbRefreshSceneMap + if $scene && $scene.is_a?(Scene_Map) + if $scene.respond_to?("miniupdate") + $scene.miniupdate + else + $scene.updatemini + end + elsif $scene && $scene.is_a?(Scene_Battle) + $scene.updatemini + end +end + +def pbUpdateSceneMap + if $scene && $scene.is_a?(Scene_Map) && !pbIsFaded? + if $scene.respond_to?("miniupdate") + $scene.miniupdate + else + $scene.updatemini + end + elsif $scene && $scene.is_a?(Scene_Battle) + $scene.updatemini + end +end + +#=============================================================================== +# +#=============================================================================== +def pbEventCommentInput(*args) + parameters = [] + list = *args[0].list # Event or event page + elements = *args[1] # Number of elements + trigger = *args[2] # Trigger + return nil if list == nil + return nil unless list.is_a?(Array) + for item in list + next unless item.code == 108 || item.code == 408 + if item.parameters[0] == trigger + start = list.index(item) + 1 + finish = start + elements + for id in start...finish + next if !list[id] + parameters.push(list[id].parameters[0]) + end + return parameters + end + end + return nil +end + +def pbCurrentEventCommentInput(elements, trigger) + return nil if !pbMapInterpreterRunning? + event = pbMapInterpreter.get_character(0) + return nil if !event + return pbEventCommentInput(event, elements, trigger) +end + +#=============================================================================== +# +#=============================================================================== +class ChooseNumberParams + def initialize + @maxDigits = 0 + @minNumber = 0 + @maxNumber = 0 + @skin = nil + @messageSkin = nil + @negativesAllowed = false + @initialNumber = 0 + @cancelNumber = nil + end + + def setMessageSkin(value) + @messageSkin = value + end + + def messageSkin # Set the full path for the message's window skin + @messageSkin + end + + def setSkin(value) + @skin = value + end + + def skin + @skin + end + + def setNegativesAllowed(value) + @negativeAllowed = value + end + + def negativesAllowed + @negativeAllowed ? true : false + end + + def setRange(minNumber, maxNumber) + maxNumber = minNumber if minNumber > maxNumber + @maxDigits = 0 + @minNumber = minNumber + @maxNumber = maxNumber + end + + def setDefaultValue(number) + @initialNumber = number + @cancelNumber = nil + end + + def setInitialValue(number) + @initialNumber = number + end + + def setCancelValue(number) + @cancelNumber = number + end + + def initialNumber + return clamp(@initialNumber, self.minNumber, self.maxNumber) + end + + def cancelNumber + return @cancelNumber || self.initialNumber + end + + def minNumber + ret = 0 + if @maxDigits > 0 + ret = -((10 ** @maxDigits) - 1) + else + ret = @minNumber + end + ret = 0 if !@negativeAllowed && ret < 0 + return ret + end + + def maxNumber + ret = 0 + if @maxDigits > 0 + ret = ((10 ** @maxDigits) - 1) + else + ret = @maxNumber + end + ret = 0 if !@negativeAllowed && ret < 0 + return ret + end + + def setMaxDigits(value) + @maxDigits = [1, value].max + end + + def maxDigits + if @maxDigits > 0 + return @maxDigits + else + return [numDigits(self.minNumber), numDigits(self.maxNumber)].max + end + end + + private + + def clamp(v, mn, mx) + return v < mn ? mn : (v > mx ? mx : v) + end + + def numDigits(number) + ans = 1 + number = number.abs + while number >= 10 + ans += 1 + number /= 10 + end + return ans + end +end + +def pbChooseNumber(msgwindow, params) + return 0 if !params + ret = 0 + maximum = params.maxNumber + minimum = params.minNumber + defaultNumber = params.initialNumber + cancelNumber = params.cancelNumber + cmdwindow = Window_InputNumberPokemon.new(params.maxDigits) + cmdwindow.z = 99999 + cmdwindow.visible = true + cmdwindow.setSkin(params.skin) if params.skin + cmdwindow.sign = params.negativesAllowed # must be set before number + cmdwindow.number = defaultNumber + pbPositionNearMsgWindow(cmdwindow, msgwindow, :right) + loop do + Graphics.update + Input.update + pbUpdateSceneMap + cmdwindow.update + msgwindow.update if msgwindow + yield if block_given? + if Input.trigger?(Input::USE) + ret = cmdwindow.number + if ret > maximum + pbPlayBuzzerSE() + elsif ret < minimum + pbPlayBuzzerSE() + else + pbPlayDecisionSE() + break + end + elsif Input.trigger?(Input::BACK) + pbPlayCancelSE() + ret = cancelNumber + break + end + end + cmdwindow.dispose + Input.update + return ret +end + +#=============================================================================== +# +#=============================================================================== +class FaceWindowVX < SpriteWindow_Base + def initialize(face) + super(0, 0, 128, 128) + faceinfo = face.split(",") + facefile = pbResolveBitmap("Graphics/Faces/" + faceinfo[0]) + facefile = pbResolveBitmap("Graphics/Pictures/" + faceinfo[0]) if !facefile + self.contents.dispose if self.contents + @faceIndex = faceinfo[1].to_i + @facebitmaptmp = AnimatedBitmap.new(facefile) + @facebitmap = BitmapWrapper.new(96, 96) + @facebitmap.blt(0, 0, @facebitmaptmp.bitmap, Rect.new( + (@faceIndex % 4) * 96, + (@faceIndex / 4) * 96, 96, 96 + )) + self.contents = @facebitmap + end + + def update + super + if @facebitmaptmp.totalFrames > 1 + @facebitmaptmp.update + @facebitmap.blt(0, 0, @facebitmaptmp.bitmap, Rect.new( + (@faceIndex % 4) * 96, + (@faceIndex / 4) * 96, 96, 96 + )) + end + end + + def dispose + @facebitmaptmp.dispose + @facebitmap.dispose if @facebitmap + super + end +end + +#=============================================================================== +# +#=============================================================================== +def pbGetBasicMapNameFromId(id) + begin + map = pbLoadMapInfos + return "" if !map + return map[id].name + rescue + return "" + end +end + +def pbGetMapNameFromId(id) + map = pbGetBasicMapNameFromId(id) + map.gsub!(/\\PN/, $Trainer.name) if $Trainer + return map +end + +def pbCsvField!(str) + ret = "" + str.sub!(/\A\s*/, "") + if str[0, 1] == "\"" + str[0, 1] = "" + escaped = false + fieldbytes = 0 + str.scan(/./) do |s| + fieldbytes += s.length + break if s == "\"" && !escaped + if s == "\\" && !escaped + escaped = true + else + ret += s + escaped = false + end + end + str[0, fieldbytes] = "" + if !str[/\A\s*,/] && !str[/\A\s*$/] + raise _INTL("Invalid quoted field (in: {1})", ret) + end + str[0, str.length] = $~.post_match + else + if str[/,/] + str[0, str.length] = $~.post_match + ret = $~.pre_match + else + ret = str.clone + str[0, str.length] = "" + end + ret.gsub!(/\s+$/, "") + end + return ret +end + +def pbCsvPosInt!(str) + ret = pbCsvField!(str) + if !ret[/\A\d+$/] + raise _INTL("Field {1} is not a positive integer", ret) + end + return ret.to_i +end + +#=============================================================================== +# Money and coins windows +#=============================================================================== +def pbGetGoldString + moneyString = "" + begin + moneyString = _INTL("${1}", $Trainer.money.to_s_formatted) + rescue + if $data_system.respond_to?("words") + moneyString = _INTL("{1} {2}", $game_party.gold, $data_system.words.gold) + else + moneyString = _INTL("{1} {2}", $game_party.gold, Vocab.gold) + end + end + return moneyString +end + +def pbDisplayGoldWindow(msgwindow) + moneyString = pbGetGoldString() + goldwindow = Window_AdvancedTextPokemon.new(_INTL("Money:\n{1}", moneyString)) + goldwindow.setSkin("Graphics/Windowskins/goldskin") + goldwindow.resizeToFit(goldwindow.text, Graphics.width) + goldwindow.width = 160 if goldwindow.width <= 160 + if msgwindow.y <= 10 + goldwindow.y = Graphics.height - goldwindow.height + else + goldwindow.y = 0 + end + goldwindow.viewport = msgwindow.viewport + goldwindow.z = msgwindow.z + return goldwindow +end + +def pbDisplayBattleFactoryPointsWindow(msgwindow) + pbDisplayVariableWindow(msgwindow, "Tokens", VAR_BATTLE_FACTORY_TOKENS) +end + +def pbDisplayVariableWindow(msgwindow, name, variable_id) + pointsString = $game_variables[variable_id].to_s + pointswindow = Window_AdvancedTextPokemon.new(_INTL("{1}:\n{2}", name, pointsString)) + pointswindow.setSkin("Graphics/Windowskins/goldskin") + pointswindow.resizeToFit(pointswindow.text, Graphics.width) + pointswindow.width = 160 if pointswindow.width <= 160 + if msgwindow.y == 0 + pointswindow.y = Graphics.height - pointswindow.height + else + pointswindow.y = 0 + end + pointswindow.viewport = msgwindow.viewport + pointswindow.z = msgwindow.z + return pointswindow +end + +def pbDisplayTwoVariableWindow(msgwindow, name1, variable1_id, name2, variable2_id) + pointsString1 = $game_variables[variable1_id].to_s + pointsString2 = $game_variables[variable2_id].to_s + + pointswindow = Window_AdvancedTextPokemon.new(_INTL("{1}:{2}\n{3}:{4}", name1, pointsString1, name2, pointsString2)) + pointswindow.setSkin("Graphics/Windowskins/goldskin") + pointswindow.resizeToFit(pointswindow.text, Graphics.width) + pointswindow.width = 160 if pointswindow.width <= 160 + if msgwindow.y == 0 + pointswindow.y = Graphics.height - pointswindow.height + else + pointswindow.y = 0 + end + pointswindow.viewport = msgwindow.viewport + pointswindow.z = msgwindow.z + return pointswindow +end + +def pbDisplayHeartScalesWindow(msgwindow) + pointsString = $PokemonBag.pbQuantity(:HEARTSCALE).to_s + pointswindow = Window_AdvancedTextPokemon.new(_INTL("Heart Scales:\n{1}", pointsString)) + pointswindow.setSkin("Graphics/Windowskins/goldskin") + pointswindow.resizeToFit(pointswindow.text, Graphics.width) + pointswindow.width = 160 if pointswindow.width <= 160 + if msgwindow.y == 0 + pointswindow.y = Graphics.height - pointswindow.height + else + pointswindow.y = 0 + end + pointswindow.viewport = msgwindow.viewport + pointswindow.z = msgwindow.z + return pointswindow +end + +def pbDisplayCoinsWindow(msgwindow, goldwindow) + coinString = ($Trainer) ? $Trainer.coins.to_s_formatted : "0" + coinwindow = Window_AdvancedTextPokemon.new(_INTL("Coins:\n{1}", coinString)) + coinwindow.setSkin("Graphics/Windowskins/goldskin") + coinwindow.resizeToFit(coinwindow.text, Graphics.width) + coinwindow.width = 160 if coinwindow.width <= 160 + if msgwindow.y == 0 + coinwindow.y = (goldwindow) ? goldwindow.y - coinwindow.height : Graphics.height - coinwindow.height + else + coinwindow.y = (goldwindow) ? goldwindow.height : 0 + end + coinwindow.viewport = msgwindow.viewport + coinwindow.z = msgwindow.z + return coinwindow +end + +def pbDisplayBattlePointsWindow(msgwindow) + pointsString = ($Trainer) ? $Trainer.battle_points.to_s_formatted : "0" + pointswindow = Window_AdvancedTextPokemon.new(_INTL("Battle Points:\n{1}", pointsString)) + pointswindow.setSkin("Graphics/Windowskins/goldskin") + pointswindow.resizeToFit(pointswindow.text, Graphics.width) + pointswindow.width = 160 if pointswindow.width <= 160 + if msgwindow.y == 0 + pointswindow.y = Graphics.height - pointswindow.height + else + pointswindow.y = 0 + end + pointswindow.viewport = msgwindow.viewport + pointswindow.z = msgwindow.z + return pointswindow +end + +#=============================================================================== +# +#=============================================================================== +def pbCreateStatusWindow(viewport = nil) + msgwindow = Window_AdvancedTextPokemon.new("") + if !viewport + msgwindow.z = 99999 + else + msgwindow.viewport = viewport + end + msgwindow.visible = false + msgwindow.letterbyletter = false + pbBottomLeftLines(msgwindow, 2) + skinfile = MessageConfig.pbGetSpeechFrame() + msgwindow.setSkin(skinfile) + return msgwindow +end + +def pbCreateMessageWindow(viewport = nil, skin = nil) + msgwindow = Window_AdvancedTextPokemon.new("") + if !viewport + msgwindow.z = 99999 + else + msgwindow.viewport = viewport + end + msgwindow.visible = true + msgwindow.letterbyletter = true + msgwindow.back_opacity = MessageConfig::WINDOW_OPACITY + pbBottomLeftLines(msgwindow, 2) + $game_temp.message_window_showing = true if $game_temp + skin = MessageConfig.pbGetSpeechFrame() if !skin + msgwindow.setSkin(skin) + return msgwindow +end + +def pbDisposeMessageWindow(msgwindow) + $game_temp.message_window_showing = false if $game_temp + msgwindow.dispose +end + +#=============================================================================== +# Main message-displaying function +#=============================================================================== +def pbMessageDisplayNoSound(msgwindow, message, letterbyletter = true, commandProc = nil) + pbMessageDisplay(msgwindow, message, letterbyletter, commandProc, false) +end + +def pbMessageDisplay(msgwindow, message, letterbyletter = true, commandProc = nil, withSound = true) + return if !msgwindow + oldletterbyletter = msgwindow.letterbyletter + msgwindow.letterbyletter = (letterbyletter) ? true : false + ret = nil + commands = nil + facewindow = nil + goldwindow = nil + coinwindow = nil + battlepointswindow = nil + cmdvariable = 0 + cmdIfCancel = 0 + msgwindow.waitcount = 0 + autoresume = false + text = message.clone + msgback = nil + linecount = (Graphics.height > 400) ? 3 : 2 + ### Text replacement + text.gsub!(/\\sign\[([^\]]*)\]/i) { # \sign[something] gets turned into + next "\\op\\cl\\ts[]\\w[" + $1 + "]" # \op\cl\ts[]\w[something] + } + text.gsub!(/\\\\/, "\5") + text.gsub!(/\\1/, "\1") + if $game_actors + text.gsub!(/\\n\[([1-8])\]/i) { + m = $1.to_i + next $game_actors[m].name + } + end + text.gsub!(/\\pn/i, $Trainer.name) if $Trainer + text.gsub!(/\\pm/i, _INTL("${1}", $Trainer.money.to_s_formatted)) if $Trainer + text.gsub!(/\\n/i, "\n") + text.gsub!(/\\\[([0-9a-f]{8,8})\]/i) { "" } + text.gsub!(/\\pg/i, "\\b") if $Trainer && $Trainer.male? + text.gsub!(/\\pg/i, "\\r") if $Trainer && $Trainer.female? + text.gsub!(/\\pog/i, "\\r") if $Trainer && $Trainer.male? + text.gsub!(/\\pog/i, "\\b") if $Trainer && $Trainer.female? + text.gsub!(/\\pg/i, "") + text.gsub!(/\\pog/i, "") + text.gsub!(/\\b/i, "") + text.gsub!(/\\r/i, "") + text.gsub!(/\\[Ww]\[([^\]]*)\]/) { + w = $1.to_s + if w == "" + msgwindow.windowskin = nil + else + msgwindow.setSkin("Graphics/Windowskins/#{w}", false) + end + next "" + } + isDarkSkin = isDarkWindowskin(msgwindow.windowskin) + text.gsub!(/\\[Cc]\[([0-9]+)\]/) { + m = $1.to_i + next getSkinColor(msgwindow.windowskin, m, isDarkSkin) + } + loop do + last_text = text.clone + text.gsub!(/\\v\[([0-9]+)\]/i) { $game_variables[$1.to_i] } + break if text == last_text + end + loop do + last_text = text.clone + text.gsub!(/\\l\[([0-9]+)\]/i) { + linecount = [1, $1.to_i].max + next "" + } + break if text == last_text + end + colortag = "" + if $game_system && $game_system.respond_to?("message_frame") && + $game_system.message_frame != 0 + colortag = getSkinColor(msgwindow.windowskin, 0, true) + else + colortag = getSkinColor(msgwindow.windowskin, 0, isDarkSkin) + end + text = colortag + text + ### Controls + textchunks = [] + controls = [] + while text[/(?:\\(f|ff|ts|cl|me|se|wt|wtnp|ch)\[([^\]]*)\]|\\(g|cn|pt|ft|hs|wd|wm|op|cl|wu|\.|\||\!|\^))/i] + textchunks.push($~.pre_match) + if $~[1] + controls.push([$~[1].downcase, $~[2], -1]) + else + controls.push([$~[3].downcase, "", -1]) + end + text = $~.post_match + end + textchunks.push(text) + for chunk in textchunks + chunk.gsub!(/\005/, "\\") + end + textlen = 0 + for i in 0...controls.length + control = controls[i][0] + case control + when "wt", "wtnp", ".", "|" + textchunks[i] += "\2" + when "!" + textchunks[i] += "\1" + end + textlen += toUnformattedText(textchunks[i]).scan(/./m).length + controls[i][2] = textlen + end + text = textchunks.join("") + signWaitCount = 0 + signWaitTime = Graphics.frame_rate / 2 + haveSpecialClose = false + specialCloseSE = "" + for i in 0...controls.length + control = controls[i][0] + param = controls[i][1] + case control + when "op" + signWaitCount = signWaitTime + 1 + when "cl" + text = text.sub(/\001\z/, "") # fix: '$' can match end of line as well + haveSpecialClose = true + specialCloseSE = param + when "f" + facewindow.dispose if facewindow + facewindow = PictureWindow.new("Graphics/Pictures/#{param}") + when "ff" + facewindow.dispose if facewindow + facewindow = FaceWindowVX.new(param) + when "ch" + cmds = param.clone + cmdvariable = pbCsvPosInt!(cmds) + cmdIfCancel = pbCsvField!(cmds).to_i + commands = [] + while cmds.length > 0 + commands.push(pbCsvField!(cmds)) + end + when "wtnp", "^" + text = text.sub(/\001\z/, "") # fix: '$' can match end of line as well + when "se" + if controls[i][2] == 0 + startSE = param + controls[i] = nil + end + end + end + if withSound + if startSE != nil + pbSEPlay(pbStringToAudioFile(startSE)) + elsif signWaitCount == 0 && letterbyletter + pbPlayDecisionSE() + end + end + ########## Position message window ############## + pbRepositionMessageWindow(msgwindow, linecount) + if facewindow + pbPositionNearMsgWindow(facewindow, msgwindow, :left) + facewindow.viewport = msgwindow.viewport + facewindow.z = msgwindow.z + end + atTop = (msgwindow.y == 0) + ########## Show text ############################# + msgwindow.text = text + Graphics.frame_reset if Graphics.frame_rate > 40 + loop do + if signWaitCount > 0 + signWaitCount -= 1 + if atTop + msgwindow.y = -msgwindow.height * signWaitCount / signWaitTime + else + msgwindow.y = Graphics.height - msgwindow.height * (signWaitTime - signWaitCount) / signWaitTime + end + end + for i in 0...controls.length + next if !controls[i] + next if controls[i][2] > msgwindow.position || msgwindow.waitcount != 0 + control = controls[i][0] + param = controls[i][1] + case control + when "f" + if param.to_i > 0 + isFusion = param.to_i > NB_POKEMON + head = getBasePokemonID(param.to_i, false) + body = getBasePokemonID(param.to_i, true) + facewindow.dispose if facewindow + #path = obtainPokemonSpritePath(body, head, true) if isFusion + + spriteLoader = BattleSpriteLoader.new + facewindow = isFusion ? PictureWindow.new(spriteLoader.load_fusion_sprite(head,body)) : PictureWindow.new(spriteLoader.load_base_sprite(head)) + pbPositionNearMsgWindow(facewindow, msgwindow, :left) + facewindow.viewport = msgwindow.viewport + facewindow.z = msgwindow.z + end + when "ff" + facewindow.dispose if facewindow + facewindow = FaceWindowVX.new(param) + pbPositionNearMsgWindow(facewindow, msgwindow, :left) + facewindow.viewport = msgwindow.viewport + facewindow.z = msgwindow.z + when "g" # Display gold window + goldwindow.dispose if goldwindow + goldwindow = pbDisplayGoldWindow(msgwindow) + when "ft" # Display battle factory tokens + goldwindow.dispose if goldwindow + goldwindow = pbDisplayBattleFactoryPointsWindow(msgwindow) + when "hs" # Display heartscakes + goldwindow.dispose if goldwindow + goldwindow = pbDisplayHeartScalesWindow(msgwindow) + when "cn" # Display coins window + coinwindow.dispose if coinwindow + coinwindow = pbDisplayCoinsWindow(msgwindow, goldwindow) + when "pt" # Display battle points window + battlepointswindow.dispose if battlepointswindow + battlepointswindow = pbDisplayBattlePointsWindow(msgwindow) + when "wu" + msgwindow.y = 0 + atTop = true + msgback.y = msgwindow.y if msgback + pbPositionNearMsgWindow(facewindow, msgwindow, :left) + msgwindow.y = -msgwindow.height * signWaitCount / signWaitTime + when "wm" + atTop = false + msgwindow.y = (Graphics.height - msgwindow.height) / 2 + msgback.y = msgwindow.y if msgback + pbPositionNearMsgWindow(facewindow, msgwindow, :left) + when "wd" + atTop = false + msgwindow.y = Graphics.height - msgwindow.height + msgback.y = msgwindow.y if msgback + pbPositionNearMsgWindow(facewindow, msgwindow, :left) + msgwindow.y = Graphics.height - msgwindow.height * (signWaitTime - signWaitCount) / signWaitTime + when "ts" # Change text speed + msgwindow.textspeed = (param == "") ? -999 : param.to_i + when "." # Wait 0.25 seconds + msgwindow.waitcount += Graphics.frame_rate / 4 + when "|" # Wait 1 second + msgwindow.waitcount += Graphics.frame_rate + when "wt" # Wait X/20 seconds + param = param.sub(/\A\s+/, "").sub(/\s+\z/, "") + msgwindow.waitcount += param.to_i * Graphics.frame_rate / 20 + when "wtnp" # Wait X/20 seconds, no pause + param = param.sub(/\A\s+/, "").sub(/\s+\z/, "") + msgwindow.waitcount = param.to_i * Graphics.frame_rate / 20 + autoresume = true + when "^" # Wait, no pause + autoresume = true + when "se" # Play SE + pbSEPlay(pbStringToAudioFile(param)) + when "me" # Play ME + pbMEPlay(pbStringToAudioFile(param)) + end + controls[i] = nil + end + break if !letterbyletter + Graphics.update + Input.update + facewindow.update if facewindow + if autoresume && msgwindow.waitcount == 0 + msgwindow.resume if msgwindow.busy? + break if !msgwindow.busy? + end + if Input.trigger?(Input::USE) || Input.trigger?(Input::BACK) + if msgwindow.busy? + pbPlayDecisionSE if msgwindow.pausing? + msgwindow.resume + else + break if signWaitCount == 0 + end + end + pbUpdateSceneMap + msgwindow.update + yield if block_given? + break if (!letterbyletter || commandProc || commands) && !msgwindow.busy? + end + Input.update # Must call Input.update again to avoid extra triggers + msgwindow.letterbyletter = oldletterbyletter + if commands + $game_variables[cmdvariable] = pbShowCommands(msgwindow, commands, cmdIfCancel) + $game_map.need_refresh = true if $game_map + end + if commandProc + ret = commandProc.call(msgwindow) + end + msgback.dispose if msgback + goldwindow.dispose if goldwindow + coinwindow.dispose if coinwindow + battlepointswindow.dispose if battlepointswindow + facewindow.dispose if facewindow + if haveSpecialClose + pbSEPlay(pbStringToAudioFile(specialCloseSE)) + atTop = (msgwindow.y == 0) + for i in 0..signWaitTime + if atTop + msgwindow.y = -msgwindow.height * i / signWaitTime + else + msgwindow.y = Graphics.height - msgwindow.height * (signWaitTime - i) / signWaitTime + end + Graphics.update + Input.update + pbUpdateSceneMap + msgwindow.update + end + end + return ret +end + +#=============================================================================== +# Message-displaying functions +#=============================================================================== +def pbMessage(message, commands = nil, cmdIfCancel = 0, skin = nil, defaultCmd = 0, &block) + ret = 0 + msgwindow = pbCreateMessageWindow(nil, skin) + if commands + ret = pbMessageDisplay(msgwindow, message, true, + proc { |msgwindow| + next Kernel.pbShowCommands(msgwindow, commands, cmdIfCancel, defaultCmd, &block) + }, &block) + else + pbMessageDisplay(msgwindow, message, &block) + end + pbDisposeMessageWindow(msgwindow) + Input.update + return ret +end + +def pbMessageNoSound(message, commands = nil, cmdIfCancel = 0, skin = nil, defaultCmd = 0, &block) + ret = 0 + msgwindow = pbCreateMessageWindow(nil, skin) + if commands + ret = pbMessageDisplayNoSound(msgwindow, message, true, + proc { |msgwindow| + next Kernel.pbShowCommands(msgwindow, commands, cmdIfCancel, defaultCmd, &block) + }, &block) + else + pbMessageDisplay(msgwindow, message, &block) + end + pbDisposeMessageWindow(msgwindow) + Input.update + return ret +end + +def pbConfirmMessage(message, &block) + return (pbMessage(message, [_INTL("Yes"), _INTL("No")], 2, &block) == 0) +end + +def pbConfirmMessageSerious(message, &block) + return (pbMessage(message, [_INTL("No"), _INTL("Yes")], 1, &block) == 1) +end + +def pbMessageChooseNumber(message, params, &block) + msgwindow = pbCreateMessageWindow(nil, params.messageSkin) + ret = pbMessageDisplay(msgwindow, message, true, + proc { |msgwindow| + next pbChooseNumber(msgwindow, params, &block) + }, &block) + pbDisposeMessageWindow(msgwindow) + return ret +end + + +def pbShowCommands(msgwindow, commands = nil, cmdIfCancel = 0, defaultCmd = 0, x_offset=nil, y_offset=nil) + return 0 if !commands + $PokemonTemp.speechbubble_arrow.visible =false if $PokemonTemp.speechbubble_arrow && !$PokemonTemp.speechbubble_arrow.disposed? + if defaultCmd == 0 && ($game_variables && $game_variables[VAR_COMMAND_WINDOW_INDEX] != 0) + defaultCmd = $game_variables[VAR_COMMAND_WINDOW_INDEX] + end + cmdwindow = Window_CommandPokemonEx.new(commands) + cmdwindow.z = 99999 + cmdwindow.visible = true + cmdwindow.resizeToFit(cmdwindow.commands) + pbPositionNearMsgWindow(cmdwindow, msgwindow, :right, x_offset, y_offset) + cmdwindow.index = defaultCmd + command = 0 + loop do + Graphics.update + Input.update + cmdwindow.update + msgwindow.update if msgwindow + yield if block_given? + if Input.trigger?(Input::BACK) + if cmdIfCancel > 0 + command = cmdIfCancel - 1 + break + elsif cmdIfCancel < 0 + command = cmdIfCancel + break + end + end + if Input.trigger?(Input::USE) + command = cmdwindow.index + break + end + pbUpdateSceneMap + end + ret = command + cmdwindow.dispose + Input.update + return ret +end + +def pbShowCommandsWithHelp(msgwindow, commands, help, cmdIfCancel = 0, defaultCmd = 0) + msgwin = msgwindow + msgwin = pbCreateMessageWindow(nil) if !msgwindow + oldlbl = msgwin.letterbyletter + msgwin.letterbyletter = false + if commands + cmdwindow = Window_CommandPokemonEx.new(commands) + cmdwindow.z = 99999 + cmdwindow.visible = true + cmdwindow.resizeToFit(cmdwindow.commands) + cmdwindow.height = msgwin.y if cmdwindow.height > msgwin.y + cmdwindow.index = defaultCmd + command = 0 + msgwin.text = help[cmdwindow.index] + msgwin.width = msgwin.width # Necessary evil to make it use the proper margins + loop do + Graphics.update + Input.update + oldindex = cmdwindow.index + cmdwindow.update + if oldindex != cmdwindow.index + msgwin.text = help[cmdwindow.index] + end + msgwin.update + yield if block_given? + if Input.trigger?(Input::BACK) + if cmdIfCancel > 0 + command = cmdIfCancel - 1 + break + elsif cmdIfCancel < 0 + command = cmdIfCancel + break + end + end + if Input.trigger?(Input::USE) + command = cmdwindow.index + break + end + pbUpdateSceneMap + end + ret = command + cmdwindow.dispose + Input.update + end + msgwin.letterbyletter = oldlbl + msgwin.dispose if !msgwindow + return ret +end + +# frames is the number of 1/20 seconds to wait for +def pbMessageWaitForInput(msgwindow, frames, showPause = false) + return if !frames || frames <= 0 + msgwindow.startPause if msgwindow && showPause + frames = frames * Graphics.frame_rate / 20 + frames.times do + Graphics.update + Input.update + msgwindow.update if msgwindow + pbUpdateSceneMap + if Input.trigger?(Input::USE) || Input.trigger?(Input::BACK) + break + end + yield if block_given? + end + msgwindow.stopPause if msgwindow && showPause +end + +def pbFreeText(msgwindow, currenttext, passwordbox, maxlength, width = 240) + window = Window_TextEntry_Keyboard.new(currenttext, 0, 0, width, 64) + ret = "" + window.maxlength = maxlength + window.visible = true + window.z = 99999 + pbPositionNearMsgWindow(window, msgwindow, :right) + window.text = currenttext + window.passwordChar = "*" if passwordbox + Input.text_input = true + loop do + Graphics.update + Input.update + if Input.triggerex?(:ESCAPE) + ret = currenttext + break + elsif Input.triggerex?(:RETURN) + ret = window.text + break + end + window.update + msgwindow.update if msgwindow + yield if block_given? + end + Input.text_input = false + window.dispose + Input.update + return ret +end + +def pbMessageFreeText(message, currenttext, passwordbox, maxlength, width = 240, &block) + msgwindow = pbCreateMessageWindow + retval = pbMessageDisplay(msgwindow, message, true, + proc { |msgwindow| + next pbFreeText(msgwindow, currenttext, passwordbox, maxlength, width, &block) + }, &block) + pbDisposeMessageWindow(msgwindow) + return retval +end diff --git a/Data/Scripts/007_Objects and windows/012_TextEntry.rb b/Data/Scripts/007_Objects and windows/012_TextEntry.rb new file mode 100644 index 000000000..2e5d201a9 --- /dev/null +++ b/Data/Scripts/007_Objects and windows/012_TextEntry.rb @@ -0,0 +1,564 @@ +#=============================================================================== +# +#=============================================================================== +class CharacterEntryHelper + attr_reader :text + attr_accessor :maxlength + attr_reader :passwordChar + attr_accessor :cursor + + def initialize(text) + @maxlength=-1 + @text=text + @passwordChar="" + @cursor=text.scan(/./m).length + end + + def text=(value) + @text=value + end + + def textChars + chars=text.scan(/./m) + if @passwordChar!="" + chars.length.times { |i| chars[i] = @passwordChar } + end + return chars + end + + def passwordChar=(value) + @passwordChar=value ? value : "" + end + + def length + return self.text.scan(/./m).length + end + + def canInsert? + chars=self.text.scan(/./m) + return false if @maxlength>=0 && chars.length>=@maxlength + return true + end + + def insert(ch) + chars=self.text.scan(/./m) + return false if @maxlength>=0 && chars.length>=@maxlength + chars.insert(@cursor,ch) + @text="" + for ch in chars + @text+=ch if ch + end + @cursor+=1 + return true + end + + def canDelete? + chars=self.text.scan(/./m) + return false if chars.length<=0 || @cursor<=0 + return true + end + + def delete + chars=self.text.scan(/./m) + return false if chars.length<=0 || @cursor<=0 + chars.delete_at(@cursor-1) + @text="" + for ch in chars + @text+=ch if ch + end + @cursor-=1 + return true + end + + private + + def ensure + return if @maxlength<0 + chars=self.text.scan(/./m) + if chars.length>@maxlength && @maxlength>=0 + chars=chars[0,@maxlength] + end + @text="" + for ch in chars + @text+=ch if ch + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_TextEntry < SpriteWindow_Base + def initialize(text,x,y,width,height,heading=nil,usedarkercolor=false) + super(x,y,width,height) + colors=getDefaultTextColors(self.windowskin) + @baseColor=colors[0] + @shadowColor=colors[1] + if usedarkercolor + @baseColor=Color.new(16,24,32) + @shadowColor=Color.new(168,184,184) + end + @helper=CharacterEntryHelper.new(text) + @heading=heading + self.active=true + @frame=0 + refresh + end + + def text + @helper.text + end + + def maxlength + @helper.maxlength + end + + def passwordChar + @helper.passwordChar + end + + def text=(value) + @helper.text=value + self.refresh + end + + def passwordChar=(value) + @helper.passwordChar=value + refresh + end + + def maxlength=(value) + @helper.maxlength=value + self.refresh + end + + def insert(ch) + if @helper.insert(ch) + @frame=0 + self.refresh + return true + end + return false + end + + def delete + if @helper.delete + @frame=0 + self.refresh + return true + end + return false + end + + def update + @frame += 1 + @frame %= 20 + self.refresh if (@frame%10)==0 + return if !self.active + # Moving cursor + if Input.repeat?(Input::LEFT) && Input.press?(Input::ACTION) + if @helper.cursor > 0 + @helper.cursor -= 1 + @frame = 0 + self.refresh + end + elsif Input.repeat?(Input::RIGHT) && Input.press?(Input::ACTION) + if @helper.cursor < self.text.scan(/./m).length + @helper.cursor += 1 + @frame = 0 + self.refresh + end + elsif Input.repeat?(Input::BACK) # Backspace + self.delete if @helper.cursor > 0 + end + end + + def refresh + self.contents=pbDoEnsureBitmap(self.contents,self.width-self.borderX, + self.height-self.borderY) + bitmap=self.contents + bitmap.clear + x=0 + y=0 + if @heading + textwidth=bitmap.text_size(@heading).width + pbDrawShadowText(bitmap,x,y, textwidth+4, 32, @heading,@baseColor,@shadowColor) + y+=32 + end + x+=4 + width=self.width-self.borderX + cursorcolor=Color.new(16,24,32) + textscan=self.text.scan(/./m) + scanlength=textscan.length + @helper.cursor=scanlength if @helper.cursor>scanlength + @helper.cursor=0 if @helper.cursor<0 + startpos=@helper.cursor + fromcursor=0 + while (startpos>0) + c=(@helper.passwordChar!="") ? @helper.passwordChar : textscan[startpos-1] + fromcursor+=bitmap.text_size(c).width + break if fromcursor>width-4 + startpos-=1 + end + for i in startpos...scanlength + c=(@helper.passwordChar!="") ? @helper.passwordChar : textscan[i] + textwidth=bitmap.text_size(c).width + next if c=="\n" + # Draw text + pbDrawShadowText(bitmap,x,y, textwidth+4, 32, c,@baseColor,@shadowColor) + # Draw cursor if necessary + if ((@frame/10)&1) == 0 && i==@helper.cursor + bitmap.fill_rect(x,y+4,2,24,cursorcolor) + end + # Add x to drawn text width + x += textwidth + end + if ((@frame/10)&1) == 0 && textscan.length==@helper.cursor + bitmap.fill_rect(x,y+4,2,24,cursorcolor) + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_TextEntry_Keyboard < Window_TextEntry + def update + @frame+=1 + @frame%=20 + self.refresh if ((@frame%10)==0) + return if !self.active + # Moving cursor + if Input.triggerex?(:LEFT) || Input.repeatex?(:LEFT) + if @helper.cursor > 0 + @helper.cursor-=1 + @frame=0 + self.refresh + end + return + elsif Input.triggerex?(:RIGHT) || Input.repeatex?(:RIGHT) + if @helper.cursor < self.text.scan(/./m).length + @helper.cursor+=1 + @frame=0 + self.refresh + end + return + elsif Input.triggerex?(:BACKSPACE) || Input.repeatex?(:BACKSPACE) + self.delete if @helper.cursor>0 + return + elsif Input.triggerex?(:RETURN) || Input.triggerex?(:ESCAPE) + return + end + Input.gets.each_char { |c| insert(c) } + end +end + + + +#=============================================================================== +# +#=============================================================================== +class Window_MultilineTextEntry < SpriteWindow_Base + def initialize(text,x,y,width,height) + super(x,y,width,height) + colors=getDefaultTextColors(self.windowskin) + @baseColor=colors[0] + @shadowColor=colors[1] + @helper=CharacterEntryHelper.new(text) + @firstline=0 + @cursorLine=0 + @cursorColumn=0 + @frame=0 + self.active=true + refresh + end + + attr_reader :baseColor + attr_reader :shadowColor + + def baseColor=(value) + @baseColor=value + refresh + end + + def shadowColor=(value) + @shadowColor=value + refresh + end + + def text + @helper.text + end + + def maxlength + @helper.maxlength + end + + def text=(value) + @helper.text=value + @textchars=nil + self.refresh + end + + def maxlength=(value) + @helper.maxlength=value + @textchars=nil + self.refresh + end + + def insert(ch) + @helper.cursor=getPosFromLineAndColumn(@cursorLine,@cursorColumn) + if @helper.insert(ch) + @frame=0 + @textchars=nil + moveCursor(0,1) + self.refresh + return true + end + return false + end + + def delete + @helper.cursor=getPosFromLineAndColumn(@cursorLine,@cursorColumn) + if @helper.delete + @frame=0 + moveCursor(0,-1) # use old textchars + @textchars=nil + self.refresh + return true + end + return false + end + + def getTextChars + if !@textchars + @textchars=getLineBrokenText(self.contents,@helper.text, + self.contents.width,nil) + end + return @textchars + end + + def getTotalLines + textchars=getTextChars + return 1 if textchars.length==0 + tchar=textchars[textchars.length-1] + return tchar[5]+1 + end + + def getLineY(line) + textchars=getTextChars + return 0 if textchars.length==0 + totallines=getTotalLines() + line=0 if line<0 + line=totallines-1 if line>=totallines + maximumY=0 + for i in 0...textchars.length + thisline=textchars[i][5] + y=textchars[i][2] + return y if thisline==line + maximumY=y if maximumY=totallines + endpos=0 + for i in 0...textchars.length + thisline=textchars[i][5] + thislength=textchars[i][8] + endpos+=thislength if thisline==line + end + return endpos + end + + def getPosFromLineAndColumn(line,column) + textchars=getTextChars + return 0 if textchars.length==0 + totallines=getTotalLines() + line=0 if line<0 + line=totallines-1 if line>=totallines + endpos=0 + for i in 0...textchars.length + thisline=textchars[i][5] + thispos=textchars[i][6] + thiscolumn=textchars[i][7] + thislength=textchars[i][8] + if thisline==line + endpos=thispos+thislength +# echoln [endpos,thispos+(column-thiscolumn),textchars[i]] + if column>=thiscolumn && column<=thiscolumn+thislength && thislength>0 + return thispos+(column-thiscolumn) + end + end + end +# if endpos==0 +# echoln [totallines,line,column] +# echoln textchars +# end +# echoln "endpos=#{endpos}" + return endpos + end + + def getLastVisibleLine + getTextChars() + textheight=[1,self.contents.text_size("X").height].max + lastVisible=@firstline+((self.height-self.borderY)/textheight)-1 + return lastVisible + end + + def updateCursorPos(doRefresh) + # Calculate new cursor position + @helper.cursor=getPosFromLineAndColumn(@cursorLine,@cursorColumn) + if doRefresh + @frame=0 + self.refresh + end + @firstline=@cursorLine if @cursorLine<@firstline + lastVisible=getLastVisibleLine() + @firstline+=(@cursorLine-lastVisible) if @cursorLine>lastVisible + end + + def moveCursor(lineOffset, columnOffset) + # Move column offset first, then lines (since column offset + # can affect line offset) +# echoln ["beforemoving",@cursorLine,@cursorColumn] + totalColumns=getColumnsInLine(@cursorLine) # check current line + totalLines=getTotalLines() + oldCursorLine=@cursorLine + oldCursorColumn=@cursorColumn + @cursorColumn+=columnOffset + if @cursorColumn<0 && @cursorLine>0 + # Will happen if cursor is moved left from the beginning of a line + @cursorLine-=1 + @cursorColumn=getColumnsInLine(@cursorLine) + elsif @cursorColumn>totalColumns && @cursorLinetotalColumns + @cursorColumn=0 if @cursorColumn<0 # totalColumns can be 0 + # Move line offset + @cursorLine+=lineOffset + @cursorLine=0 if @cursorLine<0 + @cursorLine=totalLines-1 if @cursorLine>=totalLines + # Ensure column bounds again + totalColumns=getColumnsInLine(@cursorLine) + @cursorColumn=totalColumns if @cursorColumn>totalColumns + @cursorColumn=0 if @cursorColumn<0 # totalColumns can be 0 + updateCursorPos( + oldCursorLine!=@cursorLine || + oldCursorColumn!=@cursorColumn + ) +# echoln ["aftermoving",@cursorLine,@cursorColumn] + end + + def update + @frame+=1 + @frame%=20 + self.refresh if ((@frame%10)==0) + return if !self.active + # Moving cursor + if Input.triggerex?(:UP) || Input.repeatex?(:UP) + moveCursor(-1,0) + return + elsif Input.triggerex?(:DOWN) || Input.repeatex?(:DOWN) + moveCursor(1,0) + return + elsif Input.triggerex?(:LEFT) || Input.repeatex?(:LEFT) + moveCursor(0,-1) + return + elsif Input.triggerex?(:RIGHT) || Input.repeatex?(:RIGHT) + moveCursor(0,1) + return + end + if Input.press?(Input::CTRL) && Input.triggerex?(:HOME) + # Move cursor to beginning + @cursorLine=0 + @cursorColumn=0 + updateCursorPos(true) + return + elsif Input.press?(Input::CTRL) && Input.triggerex?(:END) + # Move cursor to end + @cursorLine=getTotalLines()-1 + @cursorColumn=getColumnsInLine(@cursorLine) + updateCursorPos(true) + return + elsif Input.triggerex?(:RETURN) || Input.repeatex?(:RETURN) + self.insert("\n") + return + elsif Input.triggerex?(:BACKSPACE) || Input.repeatex?(:BACKSPACE) # Backspace + self.delete + return + end + Input.gets.each_char{|c|insert(c)} + end + + def refresh + newContents=pbDoEnsureBitmap(self.contents,self.width-self.borderX, + self.height-self.borderY) + @textchars=nil if self.contents!=newContents + self.contents=newContents + bitmap=self.contents + bitmap.clear + getTextChars + height=self.height-self.borderY + cursorcolor=Color.new(0,0,0) + textchars=getTextChars() + startY=getLineY(@firstline) + for i in 0...textchars.length + thisline=textchars[i][5] + thiscolumn=textchars[i][7] + thislength=textchars[i][8] + textY=textchars[i][2]-startY + # Don't draw lines before the first or zero-length segments + next if thisline<@firstline || thislength==0 + # Don't draw lines beyond the window's height + break if textY >= height + c=textchars[i][0] + # Don't draw spaces + next if c==" " + textwidth=textchars[i][3]+4 # add 4 to prevent draw_text from stretching text + textheight=textchars[i][4] + # Draw text + pbDrawShadowText(bitmap, textchars[i][1], textY, textwidth, textheight, c, @baseColor, @shadowColor) + end + # Draw cursor + if ((@frame/10)&1) == 0 + textheight=bitmap.text_size("X").height + cursorY=(textheight*@cursorLine)-startY + cursorX=0 + for i in 0...textchars.length + thisline=textchars[i][5] + thiscolumn=textchars[i][7] + thislength=textchars[i][8] + if thisline==@cursorLine && @cursorColumn>=thiscolumn && + @cursorColumn<=thiscolumn+thislength + cursorY=textchars[i][2]-startY + cursorX=textchars[i][1] + textheight=textchars[i][4] + posToCursor=@cursorColumn-thiscolumn + if posToCursor>=0 + partialString=textchars[i][0].scan(/./m)[0,posToCursor].join("") + cursorX+=bitmap.text_size(partialString).width + end + break + end + end + cursorY+=4 + cursorHeight=[4,textheight-4,bitmap.text_size("X").height-4].max + bitmap.fill_rect(cursorX,cursorY,2,cursorHeight,cursorcolor) + end + end +end diff --git a/Data/Scripts/008_Audio/001_Audio.rb b/Data/Scripts/008_Audio/001_Audio.rb new file mode 100644 index 000000000..c0ebc8301 --- /dev/null +++ b/Data/Scripts/008_Audio/001_Audio.rb @@ -0,0 +1,171 @@ +##################################### +# Needed because RGSS doesn't call at_exit procs on exit +# Exit is not called when game is reset (using F12) +$AtExitProcs=[] if !$AtExitProcs + +def exit(code=0) + for p in $AtExitProcs + p.call + end + raise SystemExit.new(code) +end + +def at_exit(&block) + $AtExitProcs.push(Proc.new(&block)) +end + +#=============================================================================== +# Methods that determine the duration of an audio file. +#=============================================================================== +def getOggPage(file) + fgetdw = proc { |file| + (file.eof? ? 0 : (file.read(4).unpack("V")[0] || 0)) + } + dw = fgetdw.call(file) + return nil if dw != 0x5367674F + header = file.read(22) + bodysize = 0 + hdrbodysize = (file.read(1)[0].ord rescue 0) + hdrbodysize.times do + bodysize += (file.read(1)[0].ord rescue 0) + end + ret = [header, file.pos, bodysize, file.pos + bodysize] + return ret +end + +# internal function +def oggfiletime(file) + fgetdw = proc { |file| + (file.eof? ? 0 : (file.read(4).unpack("V")[0] || 0)) + } + pages = [] + page = nil + loop do + page = getOggPage(file) + break if !page + pages.push(page) + file.pos = page[3] + end + return -1 if pages.length == 0 + curserial = nil + i = -1 + pcmlengths = [] + rates = [] + for page in pages + header = page[0] + serial = header[10, 4].unpack("V") + frame = header[2, 8].unpack("C*") + frameno = frame[7] + frameno = (frameno << 8) | frame[6] + frameno = (frameno << 8) | frame[5] + frameno = (frameno << 8) | frame[4] + frameno = (frameno << 8) | frame[3] + frameno = (frameno << 8) | frame[2] + frameno = (frameno << 8) | frame[1] + frameno = (frameno << 8) | frame[0] + if serial != curserial + curserial = serial + file.pos = page[1] + packtype = (file.read(1)[0].ord rescue 0) + string = file.read(6) + return -1 if string != "vorbis" + return -1 if packtype != 1 + i += 1 + version = fgetdw.call(file) + return -1 if version != 0 + rates[i] = fgetdw.call(file) + end + pcmlengths[i] = frameno + end + ret = 0.0 + for i in 0...pcmlengths.length + ret += pcmlengths[i].to_f / rates[i].to_f + end + return ret * 256.0 +end + +# Gets the length of an audio file in seconds. Supports WAV, MP3, and OGG files. +def getPlayTime(filename) + if safeExists?(filename) + return [getPlayTime2(filename), 0].max + elsif safeExists?(filename + ".wav") + return [getPlayTime2(filename + ".wav"), 0].max + elsif safeExists?(filename + ".mp3") + return [getPlayTime2(filename + ".mp3"), 0].max + elsif safeExists?(filename + ".ogg") + return [getPlayTime2(filename + ".ogg"), 0].max + end + return 0 +end + +def getPlayTime2(filename) + return -1 if !safeExists?(filename) + time = -1 + fgetdw = proc { |file| + (file.eof? ? 0 : (file.read(4).unpack("V")[0] || 0)) + } + fgetw = proc { |file| + (file.eof? ? 0 : (file.read(2).unpack("v")[0] || 0)) + } + File.open(filename, "rb") { |file| + file.pos = 0 + fdw = fgetdw.call(file) + if fdw == 0x46464952 # "RIFF" + filesize = fgetdw.call(file) + wave = fgetdw.call(file) + return -1 if wave != 0x45564157 # "WAVE" + fmt = fgetdw.call(file) + return -1 if fmt != 0x20746d66 # "fmt " + fmtsize = fgetdw.call(file) + format = fgetw.call(file) + channels = fgetw.call(file) + rate = fgetdw.call(file) + bytessec = fgetdw.call(file) + return -1 if bytessec == 0 + bytessample = fgetw.call(file) + bitssample = fgetw.call(file) + data = fgetdw.call(file) + return -1 if data != 0x61746164 # "data" + datasize = fgetdw.call(file) + time = (datasize*1.0)/bytessec + return time + elsif fdw == 0x5367674F # "OggS" + file.pos = 0 + time = oggfiletime(file) + return time + end + file.pos = 0 + # Find the length of an MP3 file + while true + rstr = "" + ateof = false + while !file.eof? + if (file.read(1)[0] rescue 0) == 0xFF + begin + rstr = file.read(3) + rescue + ateof = true + end + break + end + end + break if ateof || !rstr || rstr.length != 3 + if rstr[0] == 0xFB + t = rstr[1] >> 4 + next if t == 0 || t == 15 + freqs = [44100, 22050, 11025, 48000] + bitrates = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] + bitrate = bitrates[t] + t = (rstr[1] >> 2) & 3 + freq = freqs[t] + t = (rstr[1] >> 1) & 1 + filesize = FileTest.size(filename) + frameLength = ((144000 * bitrate) / freq) + t + numFrames = filesize / (frameLength + 4) + time = (numFrames * 1152.0 / freq) + break + end + end + } + return time +end diff --git a/Data/Scripts/008_Audio/002_Audio_Play.rb b/Data/Scripts/008_Audio/002_Audio_Play.rb new file mode 100644 index 000000000..a5b4d43cd --- /dev/null +++ b/Data/Scripts/008_Audio/002_Audio_Play.rb @@ -0,0 +1,294 @@ +def pbStringToAudioFile(str) + if str[/^(.*)\:\s*(\d+)\s*\:\s*(\d+)\s*$/] # Of the format "XXX: ###: ###" + file = $1 + volume = $2.to_i + pitch = $3.to_i + return RPG::AudioFile.new(file,volume,pitch) + elsif str[/^(.*)\:\s*(\d+)\s*$/] # Of the format "XXX: ###" + file = $1 + volume = $2.to_i + return RPG::AudioFile.new(file,volume,100) + else + return RPG::AudioFile.new(str,100,100) + end +end + +# Converts an object to an audio file. +# str -- Either a string showing the filename or an RPG::AudioFile object. +# Possible formats for _str_: +# filename volume and pitch 100 +# filename:volume pitch 100 +# filename:volume:pitch +# volume -- Volume of the file, up to 100 +# pitch -- Pitch of the file, normally 100 +def pbResolveAudioFile(str,volume=nil,pitch=nil) + if str.is_a?(String) + str = pbStringToAudioFile(str) + str.volume = volume || 100 + str.pitch = pitch || 100 + end + if str.is_a?(RPG::AudioFile) + if volume || pitch + return RPG::AudioFile.new(str.name,volume || str.volume || 100 , + pitch || str.pitch || 100) + else + return str + end + end + return str +end + +################################################################################ + +# Plays a BGM file. +# param -- Either a string showing the filename +# (relative to Audio/BGM/) or an RPG::AudioFile object. +# Possible formats for _param_: +# filename volume and pitch 100 +# filename:volume pitch 100 +# filename:volume:pitch +# volume -- Volume of the file, up to 100 +# pitch -- Pitch of the file, normally 100 +def pbBGMPlay(param,volume=nil,pitch=nil) + return if !param + param=pbResolveAudioFile(param,volume,pitch) + param = pbResolveAudioFile("ultra_metropolis", volume, pitch) if darknessEffectOnCurrentMap() && !$PokemonTemp.during_battle + if param.name && param.name!="" + if $game_system && $game_system.respond_to?("bgm_play") + $game_system.bgm_play(param) + return + elsif (RPG.const_defined?(:BGM) rescue false) + b=RPG::BGM.new(param.name,param.volume,param.pitch) + if b && b.respond_to?("play") + b.play + return + end + end + Audio.bgm_play(canonicalize("Audio/BGM/"+param.name),param.volume,param.pitch) + end +end + +# Fades out or stops BGM playback. 'x' is the time in seconds to fade out. +def pbBGMFade(x=0.0); pbBGMStop(x);end + +# Fades out or stops BGM playback. 'x' is the time in seconds to fade out. +def pbBGMStop(timeInSeconds=0.0) + if $game_system && timeInSeconds>0.0 && $game_system.respond_to?("bgm_fade") + $game_system.bgm_fade(timeInSeconds) + return + elsif $game_system && $game_system.respond_to?("bgm_stop") + $game_system.bgm_stop + return + elsif (RPG.const_defined?(:BGM) rescue false) + begin + (timeInSeconds>0.0) ? RPG::BGM.fade((timeInSeconds*1000).floor) : RPG::BGM.stop + return + rescue + end + end + (timeInSeconds>0.0) ? Audio.bgm_fade((timeInSeconds*1000).floor) : Audio.bgm_stop +end + +################################################################################ + +# Plays an ME file. +# param -- Either a string showing the filename +# (relative to Audio/ME/) or an RPG::AudioFile object. +# Possible formats for _param_: +# filename volume and pitch 100 +# filename:volume pitch 100 +# filename:volume:pitch +# volume -- Volume of the file, up to 100 +# pitch -- Pitch of the file, normally 100 +def pbMEPlay(param,volume=nil,pitch=nil) + echoln param + return if !param + param=pbResolveAudioFile(param,volume,pitch) + if param.name && param.name!="" + if $game_system && $game_system.respond_to?("me_play") + $game_system.me_play(param) + return + elsif (RPG.const_defined?(:ME) rescue false) + b=RPG::ME.new(param.name,param.volume,param.pitch) + if b && b.respond_to?("play") + b.play; return + end + end + Audio.me_play(canonicalize("Audio/ME/"+param.name),param.volume,param.pitch) + end +end + +# Fades out or stops ME playback. 'x' is the time in seconds to fade out. +def pbMEFade(x=0.0); pbMEStop(x);end + +# Fades out or stops ME playback. 'x' is the time in seconds to fade out. +def pbMEStop(timeInSeconds=0.0) + if $game_system && timeInSeconds>0.0 && $game_system.respond_to?("me_fade") + $game_system.me_fade(timeInSeconds) + return + elsif $game_system && $game_system.respond_to?("me_stop") + $game_system.me_stop(nil) + return + elsif (RPG.const_defined?(:ME) rescue false) + begin + (timeInSeconds>0.0) ? RPG::ME.fade((timeInSeconds*1000).floor) : RPG::ME.stop + return + rescue + end + end + (timeInSeconds>0.0) ? Audio.me_fade((timeInSeconds*1000).floor) : Audio.me_stop +end + +################################################################################ + +# Plays a BGS file. +# param -- Either a string showing the filename +# (relative to Audio/BGS/) or an RPG::AudioFile object. +# Possible formats for _param_: +# filename volume and pitch 100 +# filename:volume pitch 100 +# filename:volume:pitch +# volume -- Volume of the file, up to 100 +# pitch -- Pitch of the file, normally 100 +def pbBGSPlay(param,volume=nil,pitch=nil) + return if !param + param=pbResolveAudioFile(param,volume,pitch) + if param.name && param.name!="" + if $game_system && $game_system.respond_to?("bgs_play") + $game_system.bgs_play(param) + return + elsif (RPG.const_defined?(:BGS) rescue false) + b=RPG::BGS.new(param.name,param.volume,param.pitch) + if b && b.respond_to?("play") + b.play; return + end + end + Audio.bgs_play(canonicalize("Audio/BGS/"+param.name),param.volume,param.pitch) + end +end + +# Fades out or stops BGS playback. 'x' is the time in seconds to fade out. +def pbBGSFade(x=0.0); pbBGSStop(x);end + +# Fades out or stops BGS playback. 'x' is the time in seconds to fade out. +def pbBGSStop(timeInSeconds=0.0) + if $game_system && timeInSeconds>0.0 && $game_system.respond_to?("bgs_fade") + $game_system.bgs_fade(timeInSeconds) + return + elsif $game_system && $game_system.respond_to?("bgs_play") + $game_system.bgs_play(nil) + return + elsif (RPG.const_defined?(:BGS) rescue false) + begin + (timeInSeconds>0.0) ? RPG::BGS.fade((timeInSeconds*1000).floor) : RPG::BGS.stop + return + rescue + end + end + (timeInSeconds>0.0) ? Audio.bgs_fade((timeInSeconds*1000).floor) : Audio.bgs_stop +end + +################################################################################ + +# Plays an SE file. +# param -- Either a string showing the filename +# (relative to Audio/SE/) or an RPG::AudioFile object. +# Possible formats for _param_: +# filename volume and pitch 100 +# filename:volume pitch 100 +# filename:volume:pitch +# volume -- Volume of the file, up to 100 +# pitch -- Pitch of the file, normally 100 +def pbSEPlay(param,volume=nil,pitch=nil) + return if !param + param = pbResolveAudioFile(param,volume,pitch) + if param.name && param.name!="" + if $game_system && $game_system.respond_to?("se_play") + $game_system.se_play(param) + return + end + if (RPG.const_defined?(:SE) rescue false) + b = RPG::SE.new(param.name,param.volume,param.pitch) + if b && b.respond_to?("play") + b.play + return + end + end + Audio.se_play(canonicalize("Audio/SE/"+param.name),param.volume,param.pitch) + end +end + +# Stops SE playback. +def pbSEFade(x=0.0); pbSEStop(x);end + +# Stops SE playback. +def pbSEStop(_timeInSeconds=0.0) + if $game_system + $game_system.se_stop + elsif (RPG.const_defined?(:SE) rescue false) + RPG::SE.stop rescue nil + else + Audio.se_stop + end +end + +################################################################################ + +# Plays a sound effect that plays when the player moves the cursor. +def pbPlayCursorSE + if $data_system && $data_system.respond_to?("cursor_se") && + $data_system.cursor_se && $data_system.cursor_se.name!="" + pbSEPlay($data_system.cursor_se) + elsif $data_system && $data_system.respond_to?("sounds") && + $data_system.sounds && $data_system.sounds[0] && $data_system.sounds[0].name!="" + pbSEPlay($data_system.sounds[0]) + elsif FileTest.audio_exist?("Audio/SE/GUI sel cursor") + pbSEPlay("GUI sel cursor",80) + end +end + +# Plays a sound effect that plays when a decision is confirmed or a choice is made. +def pbPlayDecisionSE + if $data_system && $data_system.respond_to?("decision_se") && + $data_system.decision_se && $data_system.decision_se.name!="" + pbSEPlay($data_system.decision_se) + elsif $data_system && $data_system.respond_to?("sounds") && + $data_system.sounds && $data_system.sounds[1] && $data_system.sounds[1].name!="" + pbSEPlay($data_system.sounds[1]) + elsif FileTest.audio_exist?("Audio/SE/GUI sel decision") + pbSEPlay("GUI sel decision",80) + end +end + +# Plays a sound effect that plays when a choice is canceled. +def pbPlayCancelSE + if $data_system && $data_system.respond_to?("cancel_se") && + $data_system.cancel_se && $data_system.cancel_se.name!="" + pbSEPlay($data_system.cancel_se) + elsif $data_system && $data_system.respond_to?("sounds") && + $data_system.sounds && $data_system.sounds[2] && $data_system.sounds[2].name!="" + pbSEPlay($data_system.sounds[2]) + elsif FileTest.audio_exist?("Audio/SE/GUI sel cancel") + pbSEPlay("GUI sel cancel",80) + end +end + +# Plays a buzzer sound effect. +def pbPlayBuzzerSE + if $data_system && $data_system.respond_to?("buzzer_se") && + $data_system.buzzer_se && $data_system.buzzer_se.name!="" + pbSEPlay($data_system.buzzer_se) + elsif $data_system && $data_system.respond_to?("sounds") && + $data_system.sounds && $data_system.sounds[3] && $data_system.sounds[3].name!="" + pbSEPlay($data_system.sounds[3]) + elsif FileTest.audio_exist?("Audio/SE/GUI sel buzzer") + pbSEPlay("GUI sel buzzer",80) + end +end + +# Plays a sound effect that plays when the player moves the cursor. +def pbPlayCloseMenuSE + if FileTest.audio_exist?("Audio/SE/GUI menu close") + pbSEPlay("GUI menu close",80) + end +end diff --git a/Data/Scripts/009_Scenes/001_Transitions.rb b/Data/Scripts/009_Scenes/001_Transitions.rb new file mode 100644 index 000000000..4222ab80d --- /dev/null +++ b/Data/Scripts/009_Scenes/001_Transitions.rb @@ -0,0 +1,1627 @@ +module Graphics + @@transition = nil + STOP_WHILE_TRANSITION = true + + unless defined?(transition_KGC_SpecialTransition) + class << Graphics + alias transition_KGC_SpecialTransition transition + end + + class << Graphics + alias update_KGC_SpecialTransition update + end + end + + def self.transition(duration = 8, filename = "", vague = 20) + duration = duration.floor + if judge_special_transition(duration, filename) + duration = 0 + filename = "" + end + begin + transition_KGC_SpecialTransition(duration, filename, vague) + rescue Exception + if filename != "" + transition_KGC_SpecialTransition(duration, "", vague) + end + end + if STOP_WHILE_TRANSITION && !@_interrupt_transition + while @@transition && !@@transition.disposed? + update + end + end + end + + def self.update + update_KGC_SpecialTransition +=begin + if Graphics.frame_count % 40 == 0 + count = 0 + ObjectSpace.each_object(Object) { |o| count += 1 } + echoln("Objects: #{count}") + end +=end + @@transition.update if @@transition && !@@transition.disposed? + @@transition = nil if @@transition && @@transition.disposed? + end + + def self.judge_special_transition(duration,filename) + return false if @_interrupt_transition + ret = true + if @@transition && !@@transition.disposed? + @@transition.dispose + @@transition = nil + end + dc = File.basename(filename).downcase + case dc + # Other coded transitions + when "breakingglass" then @@transition = Transitions::BreakingGlass.new(duration) + when "rotatingpieces" then @@transition = Transitions::ShrinkingPieces.new(duration, true) + when "shrinkingpieces" then @@transition = Transitions::ShrinkingPieces.new(duration, false) + when "splash" then @@transition = Transitions::SplashTransition.new(duration) + when "random_stripe_v" then @@transition = Transitions::RandomStripeTransition.new(duration, 0) + when "random_stripe_h" then @@transition = Transitions::RandomStripeTransition.new(duration, 1) + when "zoomin" then @@transition = Transitions::ZoomInTransition.new(duration) + when "scrolldown" then @@transition = Transitions::ScrollScreen.new(duration, 2) + when "scrollleft" then @@transition = Transitions::ScrollScreen.new(duration, 4) + when "scrollright" then @@transition = Transitions::ScrollScreen.new(duration, 6) + when "scrollup" then @@transition = Transitions::ScrollScreen.new(duration, 8) + when "scrolldownleft" then @@transition = Transitions::ScrollScreen.new(duration, 1) + when "scrolldownright" then @@transition = Transitions::ScrollScreen.new(duration, 3) + when "scrollupleft" then @@transition = Transitions::ScrollScreen.new(duration, 7) + when "scrollupright" then @@transition = Transitions::ScrollScreen.new(duration, 9) + when "mosaic" then @@transition = Transitions::MosaicTransition.new(duration) + # HGSS transitions + when "snakesquares" then @@transition = Transitions::SnakeSquares.new(duration) + when "diagonalbubbletl" then @@transition = Transitions::DiagonalBubble.new(duration, 0) + when "diagonalbubbletr" then @@transition = Transitions::DiagonalBubble.new(duration, 1) + when "diagonalbubblebl" then @@transition = Transitions::DiagonalBubble.new(duration, 2) + when "diagonalbubblebr" then @@transition = Transitions::DiagonalBubble.new(duration, 3) + when "risingsplash" then @@transition = Transitions::RisingSplash.new(duration) + when "twoballpass" then @@transition = Transitions::TwoBallPass.new(duration) + when "spinballsplit" then @@transition = Transitions::SpinBallSplit.new(duration) + when "threeballdown" then @@transition = Transitions::ThreeBallDown.new(duration) + when "balldown" then @@transition = Transitions::BallDown.new(duration) + when "wavythreeballup" then @@transition = Transitions::WavyThreeBallUp.new(duration) + when "wavyspinball" then @@transition = Transitions::WavySpinBall.new(duration) + when "fourballburst" then @@transition = Transitions::FourBallBurst.new(duration) + # Graphic transitions + when "fadetoblack" then @@transition = Transitions::FadeToBlack.new(duration) + when "" then @@transition = Transitions::FadeFromBlack.new(duration) + else ret = false + end + Graphics.frame_reset if ret + return ret + end +end + + + +#=============================================================================== +# Screen transition classes +#=============================================================================== +module Transitions + #============================================================================= + # + #============================================================================= + class BreakingGlass + def initialize(numframes) + @disposed = false + @numframes = numframes + @opacitychange = (numframes<=0) ? 255 : 255.0/numframes + cx = 6 + cy = 5 + @bitmap = Graphics.snap_to_bitmap + if !@bitmap + @disposed = true + return + end + width = @bitmap.width/cx + height = @bitmap.height/cy + @numtiles = cx*cy + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprites = [] + @offset = [] + @y = [] + for i in 0...@numtiles + @sprites[i] = Sprite.new(@viewport) + @sprites[i].bitmap = @bitmap + @sprites[i].x = width*(i%cx) + @sprites[i].y = height*(i/cx) + @sprites[i].src_rect.set(@sprites[i].x,@sprites[i].y,width,height) + @offset[i] = (rand(100)+1)*3.0/100.0 + @y[i] = @sprites[i].y + end + end + + def disposed?; @disposed; end + + def dispose + if !disposed? + @bitmap.dispose + for i in 0...@numtiles + @sprites[i].visible = false + @sprites[i].dispose + end + @sprites.clear + @viewport.dispose if @viewport + @disposed = true + end + end + + def update + return if disposed? + continue = false + for i in 0...@numtiles + @sprites[i].opacity -= @opacitychange + @y[i] += @offset[i] + @sprites[i].y = @y[i] + continue = true if @sprites[i].opacity>0 + end + self.dispose if !continue + end + end + + #============================================================================= + # + #============================================================================= + class ShrinkingPieces + def initialize(numframes,rotation) + @disposed = false + @rotation = rotation + @numframes = numframes + @opacitychange = (numframes<=0) ? 255 : 255.0/numframes + cx = 6 + cy = 5 + @bitmap = Graphics.snap_to_bitmap + if !@bitmap + @disposed = true + return + end + width = @bitmap.width/cx + height = @bitmap.height/cy + @numtiles = cx*cy + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprites = [] + for i in 0...@numtiles + @sprites[i] = Sprite.new(@viewport) + @sprites[i].bitmap = @bitmap + @sprites[i].ox = width/2 + @sprites[i].oy = height/2 + @sprites[i].x = width*(i%cx)+@sprites[i].ox + @sprites[i].y = height*(i/cx)+@sprites[i].oy + @sprites[i].src_rect.set(width*(i%cx),height*(i/cx),width,height) + end + end + + def disposed?; @disposed; end + + def dispose + if !disposed? + @bitmap.dispose + for i in 0...@numtiles + @sprites[i].visible = false + @sprites[i].dispose + end + @sprites.clear + @viewport.dispose if @viewport + @disposed = true + end + end + + def update + return if disposed? + continue = false + for i in 0...@numtiles + @sprites[i].opacity -= @opacitychange + if @rotation + @sprites[i].angle += 40 + @sprites[i].angle %= 360 + end + @sprites[i].zoom_x = @sprites[i].opacity/255.0 + @sprites[i].zoom_y = @sprites[i].opacity/255.0 + continue = true if @sprites[i].opacity>0 + end + self.dispose if !continue + end + end + + #============================================================================= + # + #============================================================================= + class SplashTransition + SPLASH_SIZE = 32 + + def initialize(numframes,vague=9.6) + @duration = numframes + @numframes = numframes + @splash_dir = [] + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @buffer = Graphics.snap_to_bitmap + if !@buffer + @disposed = true + return + end + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprite = Sprite.new(@viewport) + @sprite.bitmap = Bitmap.new(Graphics.width, Graphics.height) + size = SPLASH_SIZE + size = [size,1].max + cells = Graphics.width*Graphics.height/(size**2) + rows = Graphics.width/size + rect = Rect.new(0,0,size,size) + mag = 40.0/@numframes + cells.times { |i| + rect.x = i%rows*size + rect.y = i/rows*size + x = rect.x/size-(rows>>1) + y = rect.y/size-((cells/rows)>>1) + r = Math.sqrt(x**2+y**2)/vague + @splash_dir[i] = [] + if r!=0 + @splash_dir[i][0] = x/r + @splash_dir[i][1] = y/r + else + @splash_dir[i][0] = (x!= 0) ? x*1.5 : pmrand*vague + @splash_dir[i][1] = (y!= 0) ? y*1.5 : pmrand*vague + end + @splash_dir[i][0] += (rand-0.5)*vague + @splash_dir[i][1] += (rand-0.5)*vague + @splash_dir[i][0] *= mag + @splash_dir[i][1] *= mag + } + @sprite.bitmap.blt(0,0,@buffer,@buffer.rect) + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @sprite.visible = false + @sprite.bitmap.dispose + @sprite.dispose + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + size = SPLASH_SIZE + cells = Graphics.width*Graphics.height/(size**2) + rows = Graphics.width/size + rect = Rect.new(0,0,size,size) + buffer = @buffer + sprite = @sprite + phase = @numframes-@duration + sprite.bitmap.clear + cells.times { |i| + rect.x = (i%rows)*size + rect.y = (i/rows)*size + dx = rect.x+@splash_dir[i][0]*phase + dy = rect.y+@splash_dir[i][1]*phase + sprite.bitmap.blt(dx,dy,buffer,rect) + } + sprite.opacity = 384*@duration/@numframes + @duration -= 1 + end + end + + private + + def pmrand + return (rand(2)==0) ? 1 : -1 + end + end + + #============================================================================= + # + #============================================================================= + class RandomStripeTransition + RAND_STRIPE_SIZE = 2 + + def initialize(numframes,direction) + @duration = numframes + @numframes = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @buffer = Graphics.snap_to_bitmap + if !@buffer + @disposed = true + return + end + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprite = Sprite.new(@viewport) + @sprite.bitmap = Bitmap.new(Graphics.width,Graphics.height) + ########## + @direction = direction + size = RAND_STRIPE_SIZE + bands = ((@direction==0) ? Graphics.width : Graphics.height)/size + @rand_stripe_deleted = [] + @rand_stripe_deleted_count = 0 + ary = (0...bands).to_a + @rand_stripe_index_array = ary.sort_by { rand } + ########## + @sprite.bitmap.blt(0,0,@buffer,@buffer.rect) + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @sprite.visible = false + @sprite.bitmap.dispose + @sprite.dispose + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + dir = @direction + size = RAND_STRIPE_SIZE + bands = ((dir==0) ? Graphics.width : Graphics.height)/size + rect = Rect.new(0,0,(dir==0) ? size : Graphics.width,(dir==0) ? Graphics.height : size) + buffer = @buffer + sprite = @sprite + count = (bands-bands*@duration/@numframes)-@rand_stripe_deleted_count + while count > 0 + @rand_stripe_deleted[@rand_stripe_index_array.pop] = true + @rand_stripe_deleted_count += 1 + count -= 1 + end + sprite.bitmap.clear + bands.to_i.times { |i| + unless @rand_stripe_deleted[i] + if dir==0 + rect.x = i*size + sprite.bitmap.blt(rect.x,0,buffer,rect) + else + rect.y = i*size + sprite.bitmap.blt(0,rect.y,buffer,rect) + end + end + } + @duration -= 1 + end + end + end + + #============================================================================= + # + #============================================================================= + class ZoomInTransition + def initialize(numframes) + @duration = numframes + @numframes = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @buffer = Graphics.snap_to_bitmap + if !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @sprite = Sprite.new(@viewport) + @sprite.bitmap = @buffer + @sprite.ox = @width/2 + @sprite.oy = @height/2 + @sprite.x = @width/2 + @sprite.y = @height/2 + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @sprite.dispose if @sprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + @sprite.zoom_x += 0.2 + @sprite.zoom_y += 0.2 + @sprite.opacity = (@duration-1)*255/@numframes + @duration -= 1 + end + end + end + + #============================================================================= + # + #============================================================================= + class ScrollScreen + def initialize(numframes,direction) + @numframes = numframes + @duration = numframes + @dir = direction + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @buffer = Graphics.snap_to_bitmap + if !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @sprite = Sprite.new(@viewport) + @sprite.bitmap = @buffer + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @sprite.dispose if @sprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + case @dir + when 1 # down left + @sprite.y += (@height/@numframes) + @sprite.x -= (@width/@numframes) + when 2 # down + @sprite.y += (@height/@numframes) + when 3 # down right + @sprite.y += (@height/@numframes) + @sprite.x += (@width/@numframes) + when 4 # left + @sprite.x -= (@width/@numframes) + when 6 # right + @sprite.x += (@width/@numframes) + when 7 # up left + @sprite.y -= (@height/@numframes) + @sprite.x -= (@width/@numframes) + when 8 # up + @sprite.y -= (@height/@numframes) + when 9 # up right + @sprite.y -= (@height/@numframes) + @sprite.x += (@width/@numframes) + end + @duration -= 1 + end + end + end + + #============================================================================= + # + #============================================================================= + class MosaicTransition + def initialize(numframes) + @duration = numframes + @numframes = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @buffer = Graphics.snap_to_bitmap + if !@buffer + @disposed = true + return + end + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprite = Sprite.new(@viewport) + @sprite.bitmap = @buffer + @bitmapclone = @buffer.clone + @bitmapclone2 = @buffer.clone + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @sprite.dispose if @sprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + @bitmapclone2.stretch_blt( + Rect.new(0,0,@buffer.width*@duration/@numframes,@buffer.height*@duration/@numframes), + @bitmapclone, + Rect.new(0,0,@buffer.width,@buffer.height)) + @buffer.stretch_blt( + Rect.new(0,0,@buffer.width,@buffer.height), + @bitmapclone2, + Rect.new(0,0,@buffer.width*@duration/@numframes,@buffer.height*@duration/@numframes)) + @duration -= 1 + end + end + end + + #============================================================================= + # + #============================================================================= + class FadeToBlack + def initialize(numframes) + @duration = numframes + @numframes = numframes + @disposed = false + if @duration<=0 + @disposed = true + return + end + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprite = BitmapSprite.new(Graphics.width,Graphics.height,@viewport) + @sprite.bitmap.fill_rect(0,0,Graphics.width,Graphics.height,Color.new(0,0,0)) + @sprite.opacity = 0 + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @sprite.dispose if @sprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + @sprite.opacity = (@numframes - @duration + 1) * 255 / @numframes + @duration -= 1 + end + end + end + + #============================================================================= + # + #============================================================================= + class FadeFromBlack + def initialize(numframes) + @duration = numframes + @numframes = numframes + @disposed = false + if @duration<=0 + @disposed = true + return + end + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprite = BitmapSprite.new(Graphics.width,Graphics.height,@viewport) + @sprite.bitmap.fill_rect(0,0,Graphics.width,Graphics.height,Color.new(0,0,0)) + @sprite.opacity = 255 + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @sprite.dispose if @sprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + @sprite.opacity = (@duration-1)*255/@numframes + @duration -= 1 + end + end + end + + #============================================================================= + # HGSS wild outdoor + #============================================================================= + class SnakeSquares + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + @bitmap = RPG::Cache.transition("black_square") + if !@bitmap + @disposed = true + return + end + width = @bitmap.width + height = @bitmap.height + cx = Graphics.width/width # 8 + cy = Graphics.height/height # 6 + @numtiles = cx*cy + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprites = [] + @frame = [] + @addzoom = 0.125*50/@numframes + for i in 0...cy + for j in 0...cx + k = i*cx+j + x = width*(j%cx) + x = (cx-1)*width-x if (i<3 && i%2==1) || (i>=3 && i%2==0) + @sprites[k] = Sprite.new(@viewport) + @sprites[k].x = x+width/2 + @sprites[k].y = height*i + @sprites[k].ox = width/2 + @sprites[k].visible = false + @sprites[k].bitmap = @bitmap + if k>=@numtiles/2 + @frame[k] = 2*(@numtiles-k-1)*(@numframes-1/@addzoom)/50 + else + @frame[k] = 2*k*(@numframes-1/@addzoom)/50 + end + end + end + end + + def disposed?; @disposed; end + + def dispose + if !disposed? + @bitmap.dispose + for i in 0...@numtiles + if @sprites[i] + @sprites[i].visible = false + @sprites[i].dispose + end + end + @sprites.clear + @viewport.dispose if @viewport + @disposed = true + end + end + + def update + return if disposed? + if @duration==0 + dispose + else + count = @numframes-@duration + for i in 0...@numtiles + if @frame[i]=count + @sprites[i].visible = true + @sprites[i].zoom_x = @addzoom*(count-@frame[i]) + @sprites[i].zoom_x = 1.0 if @sprites[i].zoom_x>1.0 + end + end + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS wild indoor day (origin=0) + # HGSS wild indoor night (origin=3) + # HGSS wild cave (origin=3) + #============================================================================= + class DiagonalBubble + def initialize(numframes,origin=0) + @numframes = numframes + @duration = numframes + @disposed = false + @bitmap = RPG::Cache.transition("black_square") + if !@bitmap + @disposed = true + return + end + width = @bitmap.width + height = @bitmap.height + cx = Graphics.width/width # 8 + cy = Graphics.height/height # 6 + @numtiles = cx*cy + @viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + @viewport.z = 99999 + @sprites = [] + @frame = [] + # 1.2, 0.6 and 0.8 determined by trigonometry of default screen size + l = 1.2*Graphics.width/(@numtiles-8) + for i in 0...cy + for j in 0...cx + k = i*cx+j + @sprites[k] = Sprite.new(@viewport) + @sprites[k].x = width*j+width/2 + @sprites[k].y = height*i+height/2 + @sprites[k].ox = width/2 + @sprites[k].oy = height/2 + @sprites[k].visible = false + @sprites[k].bitmap = @bitmap + case origin + when 1 then k = i*cx+(cx-1-j) # Top right + when 2 then k = @numtiles-1-(i*cx+(cx-1-j)) # Bottom left + when 3 then k = @numtiles-1-k # Bottom right + end + @frame[k] = ((0.6*j*width+0.8*i*height)*(@numframes/50.0)/l).floor + end + end + @addzoom = 0.125*50/@numframes + end + + def disposed?; @disposed; end + + def dispose + if !disposed? + @bitmap.dispose + for i in 0...@numtiles + if @sprites[i] + @sprites[i].visible = false + @sprites[i].dispose + end + end + @sprites.clear + @viewport.dispose if @viewport + @disposed = true + end + end + + def update + return if disposed? + if @duration==0 + dispose + else + count = @numframes-@duration + for i in 0...@numtiles + if @frame[i]=count + @sprites[i].visible = true + @sprites[i].zoom_x = @addzoom*(count-@frame[i]) + @sprites[i].zoom_x = 1.0 if @sprites[i].zoom_x>1.0 + @sprites[i].zoom_y = @sprites[i].zoom_x + end + end + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS wild water + #============================================================================= + class RisingSplash + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @bubblebitmap = RPG::Cache.transition("water_1") + @splashbitmap = RPG::Cache.transition("water_2") + @blackbitmap = RPG::Cache.transition("black_half") + @buffer = Graphics.snap_to_bitmap + if !@bubblebitmap || !@splashbitmap || !@blackbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @rearsprite = Sprite.new(@viewport) + @rearsprite.z = 1 + @rearsprite.zoom_y = 2.0 + @rearsprite.bitmap = @blackbitmap + @bgsprites = [] + rect = Rect.new(0,0,@width,2) + for i in 0...@height/2 + @bgsprites[i] = Sprite.new(@viewport) + @bgsprites[i].y = i*2 + @bgsprites[i].z = 2 + @bgsprites[i].bitmap = @buffer + rect.y = i*2 + @bgsprites[i].src_rect = rect + end + @bubblesprite = Sprite.new(@viewport) + @bubblesprite.y = @height + @bubblesprite.z = 3 + @bubblesprite.bitmap = @bubblebitmap + @splashsprite = Sprite.new(@viewport) + @splashsprite.y = @height + @splashsprite.z = 4 + @splashsprite.bitmap = @splashbitmap + @blacksprite = Sprite.new(@viewport) + @blacksprite.y = @height + @blacksprite.z = 5 + @blacksprite.zoom_y = 2.0 + @blacksprite.bitmap = @blackbitmap + @bubblesuby = @height*2/@numframes + @splashsuby = @bubblesuby*2 + @blacksuby = @height/(@numframes*0.1).floor + @angmult = 2/(@numframes/50.0) + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @bubblebitmap.dispose if @bubblebitmap + @bubblebitmap = nil + @splashbitmap.dispose if @splashbitmap + @splashbitmap = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @rearsprite.dispose if @rearsprite + for i in @bgsprites; i.dispose if i; end + @bgsprites.clear + @bubblesprite.dispose if @bubblesprite + @splashsprite.dispose if @splashsprite + @blacksprite.dispose if @blacksprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + angadd = (@numframes-@duration)*@angmult + amp = 6*angadd/8; amp = 6 if amp>6 + for i in 0...@bgsprites.length + @bgsprites[i].x = amp*Math.sin((i+angadd)*Math::PI/10) + end + @bubblesprite.x = (@width-@bubblebitmap.width)/2 + @bubblesprite.x -= 32*Math.sin((@numframes-@duration)/(@numframes/50)*3*Math::PI/60) + @bubblesprite.y -= @bubblesuby + if @duration<@numframes*0.5 + @splashsprite.y -= @splashsuby + end + if @duration<@numframes*0.1 + @blacksprite.y -= @blacksuby + @blacksprite.y = 0 if @blacksprite.y<0 + end + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS trainer outdoor day + #============================================================================= + class TwoBallPass + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @blackbitmap = RPG::Cache.transition("black_half") + @ballbitmap = RPG::Cache.transition("ball_small") + @buffer = Graphics.snap_to_bitmap + if !@blackbitmap || !@ballbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @bgsprite = Sprite.new(@viewport) + @bgsprite.x = @width/2 + @bgsprite.y = @height/2 + @bgsprite.ox = @width/2 + @bgsprite.oy = @height/2 + @bgsprite.bitmap = @buffer + @blacksprites = [] + @ballsprites = [] + for i in 0...2 + @blacksprites[i] = Sprite.new(@viewport) + @blacksprites[i].x = (1-i*2)*@width + @blacksprites[i].y = i*@height/2 + @blacksprites[i].z = 1 + @blacksprites[i].bitmap = @blackbitmap + @ballsprites[i] = Sprite.new(@viewport) + @ballsprites[i].x = (1-i)*@width + (1-i*2)*@ballbitmap.width/2 + @ballsprites[i].y = @height/2 + (i*2-1)*@ballbitmap.height/2 + @ballsprites[i].z = 2 + @ballsprites[i].ox = @ballbitmap.width/2 + @ballsprites[i].oy = @ballbitmap.height/2 + @ballsprites[i].bitmap = @ballbitmap + end + @blacksprites[2] = Sprite.new(@viewport) + @blacksprites[2].y = @height/2 + @blacksprites[2].z = 1 + @blacksprites[2].oy = @height/4 + @blacksprites[2].zoom_y = 0.0 + @blacksprites[2].bitmap = @blackbitmap + @addxmult = 2.0*@width/((@numframes*0.6)**2) + @addzoom = 0.02*50/@numframes + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + @bgsprite.dispose if @bgsprite + for i in @blacksprites; i.dispose if i; end + @blacksprites.clear + for i in @ballsprites; i.dispose if i; end + @ballsprites.clear + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + elsif @duration>=@numframes*0.6 + for i in 0...2 + @ballsprites[i].x += (2*i-1)*((@width+@ballbitmap.width)/(0.4*@numframes)) + @ballsprites[i].angle += (2*i-1)*(360.0/(0.2*@numframes)) + end + else + addx = (@numframes*0.6-@duration)*@addxmult + for i in 0...2 + @bgsprite.zoom_x += @addzoom + @bgsprite.zoom_y += @addzoom + @blacksprites[i].x += (2*i-1)*addx + end + @blacksprites[2].zoom_y += 2*addx/@width + @blacksprites[0].x = 0 if @blacksprites[0].x<0 + @blacksprites[1].x = 0 if @blacksprites[1].x>0 + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS trainer outdoor night + #============================================================================= + class SpinBallSplit + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @blackbitmap = RPG::Cache.transition("black_half") + @ballbitmap = RPG::Cache.transition("ball_large") + @buffer = Graphics.snap_to_bitmap + if !@blackbitmap || !@ballbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @bgsprites = [] + @blacksprites = [] + @ballsprites = [] + for i in 0...2 + @bgsprites[i] = Sprite.new(@viewport) + @bgsprites[i].x = @width/2 + @bgsprites[i].y = @height/2 + @bgsprites[i].ox = @width/2 + @bgsprites[i].oy = (1-i)*@height/2 + @bgsprites[i].bitmap = @buffer + @bgsprites[i].src_rect.set(0,i*@height/2,@width,@height/2) + @blacksprites[i] = Sprite.new(@viewport) + @blacksprites[i].x = (1-i*2)*@width + @blacksprites[i].y = i*@height/2 + @blacksprites[i].z = 1 + @blacksprites[i].bitmap = @blackbitmap + @ballsprites[i] = Sprite.new(@viewport) + @ballsprites[i].x = @width/2 + @ballsprites[i].y = @height/2 + @ballsprites[i].z = 2 + @ballsprites[i].ox = @ballbitmap.width/2 + @ballsprites[i].oy = (1-i)*@ballbitmap.height/2 + @ballsprites[i].zoom_x = 0.0 + @ballsprites[i].zoom_y = 0.0 + @ballsprites[i].bitmap = @ballbitmap + end + @addxmult = 2.0*@width/((@numframes*0.5)**2) + @addzoom = 0.02*50/@numframes + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + for i in @bgsprites; i.dispose if i; end + @bgsprites.clear + for i in @blacksprites; i.dispose if i; end + @blacksprites.clear + for i in @ballsprites; i.dispose if i; end + @ballsprites.clear + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + elsif @duration>=@numframes*0.6 + if @ballsprites[0].zoom_x<1.0 + @ballsprites[0].zoom_x += (1.0/(0.4*@numframes)) + @ballsprites[0].zoom_y += (1.0/(0.4*@numframes)) + @ballsprites[0].angle -= (360.0/(0.4*@numframes)) + if @ballsprites[0].zoom_x>=1.0 + for i in 0...2 + @ballsprites[i].src_rect.set(0,i*@ballbitmap.height/2, + @ballbitmap.width,@ballbitmap.height/2) + @ballsprites[i].zoom_x = @ballsprites[i].zoom_y = 1.0 + @ballsprites[i].angle = 0.0 + end + end + end + # Gap between 0.6*@numframes and 0.5*@numframes + elsif @duration<@numframes*0.5 + addx = (@numframes*0.5-@duration)*@addxmult + for i in 0...2 + @bgsprites[i].x += (2*i-1)*addx + @bgsprites[i].zoom_x += @addzoom + @bgsprites[i].zoom_y += @addzoom + @blacksprites[i].x += (2*i-1)*addx + @ballsprites[i].x += (2*i-1)*addx + end + @blacksprites[0].x = 0 if @blacksprites[0].x<0 + @blacksprites[1].x = 0 if @blacksprites[1].x>0 + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS trainer indoor day + #============================================================================= + class ThreeBallDown + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @blackbitmap = RPG::Cache.transition("black_square") + @ballbitmap = RPG::Cache.transition("ball_small") + @buffer = Graphics.snap_to_bitmap + if !@blackbitmap || !@ballbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + cx = Graphics.width/@blackbitmap.width # 8 + cy = Graphics.height/@blackbitmap.height # 6 + @numtiles = cx*cy + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @bgsprite = Sprite.new(@viewport) + @bgsprite.x = @width/2 + @bgsprite.y = @height/2 + @bgsprite.ox = @width/2 + @bgsprite.oy = @height/2 + @bgsprite.bitmap = @buffer + @frame = [] + @blacksprites = [] + for i in 0...cy + for j in 0...cx + k = i*cx+j + @blacksprites[k] = Sprite.new(@viewport) + @blacksprites[k].x = @blackbitmap.width*j + @blacksprites[k].y = @blackbitmap.height*i + @blacksprites[k].visible = false + @blacksprites[k].bitmap = @blackbitmap + @frame[k] = (((cy-i-1)*8+[0,4,1,6,7,2,5,3][j])*(@numframes*0.75)/@numtiles).floor + end + end + @ballsprites = [] + for i in 0...3 + @ballsprites[i] = Sprite.new(@viewport) + @ballsprites[i].x = 96+i*160 + @ballsprites[i].y = -@ballbitmap.height-[400,0,100][i] + @ballsprites[i].z = 2 + @ballsprites[i].ox = @ballbitmap.width/2 + @ballsprites[i].oy = @ballbitmap.height/2 + @ballsprites[i].bitmap = @ballbitmap + end + @addyball = (@height+400+@ballbitmap.height*2)/(0.25*@numframes) + @addangle = 1.5*360/(0.25*@numframes) + @addzoom = 0.02*50/@numframes + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + @bgsprite.dispose if @bgsprite + for i in @blacksprites; i.dispose if i; end + @blacksprites.clear + for i in @ballsprites; i.dispose if i; end + @ballsprites.clear + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + elsif @duration>=@numframes*0.75 + for i in 0...@ballsprites.length + @ballsprites[i].y += @addyball + @ballsprites[i].angle -= @addangle*([1,-1][(i==2) ? 1 : 0]) + end + else + count = (@numframes*0.75).floor-@duration + for i in 0...@numtiles + @blacksprites[i].visible = true if @frame[i]<=count + end + @bgsprite.zoom_x += @addzoom + @bgsprite.zoom_y += @addzoom + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS trainer indoor night + # HGSS trainer cave + #============================================================================= + class BallDown + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @blackbitmap = RPG::Cache.transition("black_half") + @curvebitmap = RPG::Cache.transition("black_curve") + @ballbitmap = RPG::Cache.transition("ball_small") + @buffer = Graphics.snap_to_bitmap + if !@blackbitmap || !@curvebitmap || !@ballbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @bgsprite = Sprite.new(@viewport) + @bgsprite.x = @width/2 + @bgsprite.y = @height/2 + @bgsprite.ox = @width/2 + @bgsprite.oy = @height/2 + @bgsprite.bitmap = @buffer + @blacksprites = [] + @blacksprites[0] = Sprite.new(@viewport) + @blacksprites[0].y = -@curvebitmap.height + @blacksprites[0].z = 1 + @blacksprites[0].oy = @blackbitmap.height + @blacksprites[0].zoom_y = 2.0 + @blacksprites[0].bitmap = @blackbitmap + @blacksprites[1] = Sprite.new(@viewport) + @blacksprites[1].y = -@curvebitmap.height + @blacksprites[1].z = 1 + @blacksprites[1].bitmap = @curvebitmap + @ballsprite = Sprite.new(@viewport) + @ballsprite.x = @width/2 + @ballsprite.y = -@ballbitmap.height/2 + @ballsprite.z = 2 + @ballsprite.ox = @ballbitmap.width/2 + @ballsprite.oy = @ballbitmap.height/2 + @ballsprite.zoom_x = 0.25 + @ballsprite.zoom_y = 0.25 + @ballsprite.bitmap = @ballbitmap + @addyball = (@height+@ballbitmap.height*2.5)/(0.5*@numframes) + @addangle = 1.5*360/(0.5*@numframes) + @addzoomball = 2.5/(0.5*@numframes) + @addy = (@height+@curvebitmap.height)/(@numframes*0.5) + @addzoom = 0.02*50/@numframes + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @curvebitmap.dispose if @curvebitmap + @curvebitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + @bgsprite.dispose if @bgsprite + for i in @blacksprites; i.dispose if i; end + @blacksprites.clear + @ballsprite.dispose + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + elsif @duration>=@numframes*0.5 + @ballsprite.y += @addyball + @ballsprite.angle -= @addangle + @ballsprite.zoom_x += @addzoomball + @ballsprite.zoom_y += @addzoomball + else + @blacksprites[1].y += @addy + @blacksprites[0].y = @blacksprites[1].y + @bgsprite.zoom_x += @addzoom + @bgsprite.zoom_y += @addzoom + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS trainer water day + #============================================================================= + class WavyThreeBallUp + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @blackbitmap = RPG::Cache.transition("black_half") + @ballbitmap = RPG::Cache.transition("ball_small") + @buffer = Graphics.snap_to_bitmap + if !@blackbitmap || !@ballbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @rearsprite = Sprite.new(@viewport) + @rearsprite.z = 1 + @rearsprite.zoom_y = 2.0 + @rearsprite.bitmap = @blackbitmap + @bgsprites = [] + rect = Rect.new(0,0,@width,2) + for i in 0...@height/2 + @bgsprites[i] = Sprite.new(@viewport) + @bgsprites[i].y = i*2 + @bgsprites[i].z = 2 + @bgsprites[i].bitmap = @buffer + rect.y = i*2 + @bgsprites[i].src_rect = rect + end + @blacksprites = [] + @ballsprites = [] + for i in 0...3 + @blacksprites[i] = Sprite.new(@viewport) + @blacksprites[i].x = (i-1)*@width*2/3 + @blacksprites[i].y = [@height*1.5,@height*3.25,@height*2.5][i] + @blacksprites[i].z = 3 + @blacksprites[i].zoom_y = 2.0 + @blacksprites[i].bitmap = @blackbitmap + @ballsprites[i] = Sprite.new(@viewport) + @ballsprites[i].x = (2*i+1)*@width/6 + @ballsprites[i].y = [@height*1.5,@height*3.25,@height*2.5][i] + @ballsprites[i].z = 4 + @ballsprites[i].ox = @ballbitmap.width/2 + @ballsprites[i].oy = @ballbitmap.height/2 + @ballsprites[i].bitmap = @ballbitmap + end + @suby = (@height*3.5)/(@numframes*0.6) + @angmult = 4/(@numframes/50.0) + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + @rearsprite.dispose if @rearsprite + for i in @bgsprites; i.dispose if i; end + @bgsprites.clear + for i in @blacksprites; i.dispose if i; end + @blacksprites.clear + for i in @ballsprites; i.dispose if i; end + @ballsprites.clear + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + angadd = (@numframes-@duration)*@angmult + amp = 24*angadd/16; amp = 24 if amp>24 + for i in 0...@bgsprites.length + @bgsprites[i].x = amp*Math.sin((i+angadd)*Math::PI/48)*((i%2)*2-1) + end + if @duration<@numframes*0.6 + for i in 0...3 + @blacksprites[i].y -= @suby + @blacksprites[i].y = 0 if @blacksprites[i].y<0 + @ballsprites[i].y -= @suby + @ballsprites[i].angle += (2*(i%2)-1)*(360.0/(0.2*@numframes)) + end + end + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS trainer water night + #============================================================================= + class WavySpinBall + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @blackbitmap = RPG::Cache.transition("black_half") + @ballbitmap = RPG::Cache.transition("ball_large") + @buffer = Graphics.snap_to_bitmap + if !@blackbitmap || !@ballbitmap || !@buffer + @disposed = true + return + end + @width = @buffer.width + @height = @buffer.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @rearsprite = Sprite.new(@viewport) + @rearsprite.z = 1 + @rearsprite.zoom_y = 2.0 + @rearsprite.bitmap = @blackbitmap + @bgsprites = [] + rect = Rect.new(0,0,@width,2) + for i in 0...@height/2 + @bgsprites[i] = Sprite.new(@viewport) + @bgsprites[i].y = i*2 + @bgsprites[i].z = 2 + @bgsprites[i].bitmap = @buffer + rect.y = i*2 + @bgsprites[i].src_rect = rect + end + @ballsprite = Sprite.new(@viewport) + @ballsprite.x = @width/2 + @ballsprite.y = @height/2 + @ballsprite.z = 3 + @ballsprite.ox = @ballbitmap.width/2 + @ballsprite.oy = @ballbitmap.height/2 + @ballsprite.visible = false + @ballsprite.bitmap = @ballbitmap + @blacksprite = Sprite.new(@viewport) + @blacksprite.x = @width/2 + @blacksprite.y = @height/2 + @blacksprite.z = 4 + @blacksprite.ox = @blackbitmap.width/2 + @blacksprite.oy = @blackbitmap.height/2 + @blacksprite.visible = false + @blacksprite.bitmap = @blackbitmap + @angmult = 4/(@numframes/50.0) + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @buffer.dispose if @buffer + @buffer = nil + @blackbitmap.dispose if @blackbitmap + @blackbitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + @rearsprite.dispose if @rearsprite + for i in @bgsprites; i.dispose if i; end + @bgsprites.clear + @ballsprite.dispose if @ballsprite + @blacksprite.dispose if @blacksprite + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + else + angadd = (@numframes-@duration)*@angmult + amp = 24*angadd/16; amp = 24 if amp>24 + for i in 0...@bgsprites.length + @bgsprites[i].x = amp*Math.sin((i+angadd)*Math::PI/48)*((i%2)*2-1) + end + @ballsprite.visible = true + if @duration>=@numframes*0.6 + @ballsprite.opacity = 255*(@numframes-@duration)/(@numframes*0.4) + @ballsprite.angle = -360.0*(@numframes-@duration)/(@numframes*0.4) + elsif @duration<@numframes*0.5 + @blacksprite.visible = true + @blacksprite.zoom_x = (@numframes*0.5-@duration)/(@numframes*0.5) + @blacksprite.zoom_y = 2*(@numframes*0.5-@duration)/(@numframes*0.5) + end + end + @duration -= 1 + end + end + + #============================================================================= + # HGSS double trainers + #============================================================================= + class FourBallBurst + def initialize(numframes) + @numframes = numframes + @duration = numframes + @disposed = false + if @numframes<=0 + @disposed = true + return + end + @black1bitmap = RPG::Cache.transition("black_wedge_1") + @black2bitmap = RPG::Cache.transition("black_wedge_2") + @black3bitmap = RPG::Cache.transition("black_wedge_3") + @black4bitmap = RPG::Cache.transition("black_wedge_4") + @ballbitmap = RPG::Cache.transition("ball_small") + if !@black1bitmap || !@black2bitmap || !@black3bitmap || !@black4bitmap || !@ballbitmap + @disposed = true + return + end + @width = Graphics.width + @height = Graphics.height + @viewport = Viewport.new(0,0,@width,@height) + @viewport.z = 99999 + @ballsprites = [] + for i in 0...4 + @ballsprites[i] = Sprite.new(@viewport) + @ballsprites[i].x = @width/2 + @ballsprites[i].y = @height/2 + @ballsprites[i].z = [2,1,3,0][i] + @ballsprites[i].ox = @ballbitmap.width/2 + @ballsprites[i].oy = @ballbitmap.height/2 + @ballsprites[i].bitmap = @ballbitmap + end + @blacksprites = [] + for i in 0...4 + b = [@black1bitmap,@black2bitmap,@black3bitmap,@black4bitmap][i] + @blacksprites[i] = Sprite.new(@viewport) + @blacksprites[i].x = (i==1) ? 0 : @width/2 + @blacksprites[i].y = (i==2) ? 0 : @height/2 + @blacksprites[i].ox = (i%2==0) ? b.width/2 : 0 + @blacksprites[i].oy = (i%2==0) ? 0 : b.height/2 + @blacksprites[i].zoom_x = (i%2==0) ? 0.0 : 1.0 + @blacksprites[i].zoom_y = (i%2==0) ? 1.0 : 0.0 + @blacksprites[i].visible = false + @blacksprites[i].bitmap = b + end + @addxball = (@width/2+@ballbitmap.width/2)/(@numframes*0.4) + @addyball = (@height/2+@ballbitmap.height/2)/(@numframes*0.4) + @addzoom = 1.0/(@numframes*0.6) + end + + def disposed?; @disposed; end + + def dispose + return if disposed? + @black1bitmap.dispose if @black1bitmap + @black1bitmap = nil + @black2bitmap.dispose if @black2bitmap + @black2bitmap = nil + @black3bitmap.dispose if @black3bitmap + @black3bitmap = nil + @black4bitmap.dispose if @black4bitmap + @black4bitmap = nil + @ballbitmap.dispose if @ballbitmap + @ballbitmap = nil + for i in @ballsprites; i.dispose if i; end + @ballsprites.clear + for i in @blacksprites; i.dispose if i; end + @blacksprites.clear + @viewport.dispose if @viewport + @disposed = true + end + + def update + return if disposed? + if @duration==0 + dispose + elsif @duration>=@numframes*0.6 + for i in 0...@ballsprites.length + @ballsprites[i].x += (i==1) ? @addxball : (i==3) ? -@addxball : 0 + @ballsprites[i].y += (i==0) ? @addyball : (i==2) ? -@addyball : 0 + end + else + for i in 0...@blacksprites.length + @blacksprites[i].visible = true + @blacksprites[i].zoom_x += (i%2==0) ? @addzoom : 0 + @blacksprites[i].zoom_y += (i%2==0) ? 0 : @addzoom + end + end + @duration -= 1 + end + end +end diff --git a/Data/Scripts/009_Scenes/002_EventScene.rb b/Data/Scripts/009_Scenes/002_EventScene.rb new file mode 100644 index 000000000..94e9de3e1 --- /dev/null +++ b/Data/Scripts/009_Scenes/002_EventScene.rb @@ -0,0 +1,198 @@ +class PictureSprite < SpriteWrapper + def initialize(viewport, picture) + super(viewport) + @picture = picture + @pictureBitmap = nil + @customBitmap = nil + @customBitmapIsBitmap = true + @hue = 0 + update + end + + def dispose + @pictureBitmap.dispose if @pictureBitmap + super + end + + # Doesn't free the bitmap + def setCustomBitmap(bitmap) + @customBitmap = bitmap + @customBitmapIsBitmap = @customBitmap.is_a?(Bitmap) + end + + def update + super + @pictureBitmap.update if @pictureBitmap + # If picture file name is different from current one + if @customBitmap && @picture.name=="" + self.bitmap = (@customBitmapIsBitmap) ? @customBitmap : @customBitmap.bitmap + elsif @picture_name != @picture.name || @picture.hue.to_i != @hue.to_i + # Remember file name to instance variables + @picture_name = @picture.name + @hue = @picture.hue.to_i + # If file name is not empty + if @picture_name == "" + @pictureBitmap.dispose if @pictureBitmap + @pictureBitmap = nil + self.visible = false + return + end + # Get picture graphic + @pictureBitmap.dispose if @pictureBitmap + @pictureBitmap = AnimatedBitmap.new(@picture_name, @hue) + self.bitmap = (@pictureBitmap) ? @pictureBitmap.bitmap : nil + elsif @picture_name == "" + # Set sprite to invisible + self.visible = false + return + end + setPictureSprite(self,@picture) + end +end + + + +def pbTextBitmap(text, maxwidth=Graphics.width) + tmp = Bitmap.new(maxwidth,Graphics.height) + pbSetSystemFont(tmp) + drawFormattedTextEx(tmp,0,0,maxwidth,text,Color.new(248,248,248),Color.new(168,184,184)) + return tmp +end + + + +#=============================================================================== +# EventScene +#=============================================================================== +class EventScene + attr_accessor :onCTrigger,:onBTrigger,:onUpdate + + def initialize(viewport=nil) + @viewport = viewport + @onCTrigger = Event.new + @onBTrigger = Event.new + @onUpdate = Event.new + @pictures = [] + @picturesprites = [] + @usersprites = [] + @disposed = false + end + + def dispose + return if disposed? + for sprite in @picturesprites + sprite.dispose + end + for sprite in @usersprites + sprite.dispose + end + @onCTrigger.clear + @onBTrigger.clear + @onUpdate.clear + @pictures.clear + @picturesprites.clear + @usersprites.clear + @disposed = true + end + + def disposed? + return @disposed + end + + def addBitmap(x, y, bitmap) + # _bitmap_ can be a Bitmap or an AnimatedBitmap + # (update method isn't called if it's animated) + # EventScene doesn't take ownership of the passed-in bitmap + num = @pictures.length + picture = PictureEx.new(num) + picture.setXY(0,x,y) + picture.setVisible(0,true) + @pictures[num] = picture + @picturesprites[num] = PictureSprite.new(@viewport,picture) + @picturesprites[num].setCustomBitmap(bitmap) + return picture + end + + def addLabel(x, y, width, text) + addBitmap(x,y,pbTextBitmap(text,width)) + end + + def addImage(x, y, name) + num = @pictures.length + picture = PictureEx.new(num) + picture.name = name + picture.setXY(0,x,y) + picture.setVisible(0,true) + @pictures[num] = picture + @picturesprites[num] = PictureSprite.new(@viewport,picture) + return picture + end + + def addUserSprite(sprite) + @usersprites.push(sprite) + end + + def getPicture(num) + return @pictures[num] + end + + def wait(frames) + frames.times { update } + end + + def pictureWait(extraframes=0) + loop do + hasRunning = false + for pic in @pictures + hasRunning = true if pic.running? + end + break if !hasRunning + update + end + extraframes.times { update } + end + + def update + return if disposed? + Graphics.update + Input.update + for picture in @pictures + picture.update + end + for sprite in @picturesprites + sprite.update + end + for sprite in @usersprites + next if !sprite || sprite.disposed? || !sprite.is_a?(Sprite) + sprite.update + end + @onUpdate.trigger(self) + if Input.trigger?(Input::BACK) + @onBTrigger.trigger(self) + elsif Input.trigger?(Input::USE) + @onCTrigger.trigger(self) + end + end + + def main + while !disposed? + update + end + end +end + + + +#=============================================================================== +# +#=============================================================================== +def pbEventScreen(cls) + pbFadeOutIn { + viewport = Viewport.new(0,0,Graphics.width,Graphics.height) + viewport.z = 99999 + PBDebug.logonerr { + cls.new(viewport).main + } + viewport.dispose + } +end diff --git a/Data/Scripts/010_Data/001_GameData.rb b/Data/Scripts/010_Data/001_GameData.rb new file mode 100644 index 000000000..89b37d67a --- /dev/null +++ b/Data/Scripts/010_Data/001_GameData.rb @@ -0,0 +1,285 @@ +module GameData + #============================================================================= + # A mixin module for data classes which provides common class methods (called + # by GameData::Thing.method) that provide access to data held within. + # Assumes the data class's data is stored in a class constant hash called DATA. + # For data that is known by a symbol or an ID number. + #============================================================================= + module ClassMethods + def register(hash) + self::DATA[hash[:id]] = self::DATA[hash[:id_number]] = self.new(hash) + end + + # @param other [Symbol, self, String, Integer] + # @return [Boolean] whether the given other is defined as a self + def exists?(other) + return false if other.nil? + validate other => [Symbol, self, String, Integer] + other = other.id if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + + if self == GameData::Species + return !get(other).nil? + end + + return !self::DATA[other].nil? + end + + # @param other [Symbol, self, String, Integer] + # @return [self] + def get(other) + validate other => [Symbol, self, String, Integer] + + return other if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + + #B1H1 - old format (still supported) + if other.to_s.match?(/\AB\d+H\d+\z/) + species = GameData::FusedSpecies.new(other) + return species + end + + if other.to_s.include?("/") + species = GameData::FusedSpecies.new(other) + return species + end + + if other.is_a?(Integer) && self == GameData::Species + if other > NB_POKEMON + body_id = getBodyID(other) + head_id = getHeadID(other, body_id) + pokemon_id = getFusedPokemonIdFromDexNum(body_id, head_id) + return GameData::FusedSpecies.new(pokemon_id) + end + end + + if !self::DATA.has_key?(other) + #echoln _INTL("Unknown ID {1}.", other) + return self::get(:PIKACHU) + end + + #if other == :Species + + # end + + return self::DATA[other] + end + + # @param other [Symbol, self, String, Integer] + # @return [self, nil] + def try_get(other) + return nil if other.nil? + validate other => [Symbol, self, String, Integer] + return other if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + + if other.to_s.match?(/\AB\d+H\d+\z/) #old format (still supported) + species = GameData::FusedSpecies.new(other) + return species + end + + if other.to_s.include?("_x_") #new format + species = GameData::FusedSpecies.new(other) + return species + end + + if other.is_a?(Integer) && self == GameData::Species + if other > NB_POKEMON + body_id = getBodyID(other) + head_id = getHeadID(other, body_id) + pokemon_id = getFusedPokemonIdFromDexNum(body_id, head_id) + return GameData::FusedSpecies.new(pokemon_id) + end + end + + # if other.is_a?(Integer) + # p "Please switch to symbols, thanks." + # end + return (self::DATA.has_key?(other)) ? self::DATA[other] : nil + end + + # Returns the array of keys for the data. + # @return [Array] + def keys + return self::DATA.keys + end + + # Yields all data in order of their id_number. + def each + keys = self::DATA.keys.sort { |a, b| self::DATA[a].id_number <=> self::DATA[b].id_number } + keys.each { |key| yield self::DATA[key] if !key.is_a?(Integer) } + end + + def load + const_set(:DATA, load_data("Data/#{self::DATA_FILENAME}")) + end + + def save + save_data(self::DATA, "Data/#{self::DATA_FILENAME}") + end + end + + #============================================================================= + # A mixin module for data classes which provides common class methods (called + # by GameData::Thing.method) that provide access to data held within. + # Assumes the data class's data is stored in a class constant hash called DATA. + # For data that is only known by a symbol. + #============================================================================= + module ClassMethodsSymbols + def register(hash) + self::DATA[hash[:id]] = self.new(hash) + end + + # @param other [Symbol, self, String] + # @return [Boolean] whether the given other is defined as a self + def exists?(other) + return false if other.nil? + validate other => [Symbol, self, String] + other = other.id if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + return !self::DATA[other].nil? + end + + # @param other [Symbol, self, String] + # @return [self] + def get(other) + validate other => [Symbol, self, String] + return other if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + raise "Unknown ID #{other}." unless self::DATA.has_key?(other) + return self::DATA[other] + end + + # @param other [Symbol, self, String] + # @return [self, nil] + def try_get(other) + return nil if other.nil? + validate other => [Symbol, self, String] + return other if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + return (self::DATA.has_key?(other)) ? self::DATA[other] : nil + end + + # Returns the array of keys for the data. + # @return [Array] + def keys + return self::DATA.keys + end + + # Yields all data in alphabetical order. + def each + keys = self::DATA.keys.sort { |a, b| self::DATA[a].real_name <=> self::DATA[b].real_name } + keys.each { |key| yield self::DATA[key] } + end + + def load + const_set(:DATA, load_data("Data/#{self::DATA_FILENAME}")) + end + + def save + save_data(self::DATA, "Data/#{self::DATA_FILENAME}") + end + end + + #============================================================================= + # A mixin module for data classes which provides common class methods (called + # by GameData::Thing.method) that provide access to data held within. + # Assumes the data class's data is stored in a class constant hash called DATA. + # For data that is only known by an ID number. + #============================================================================= + module ClassMethodsIDNumbers + def register(hash) + self::DATA[hash[:id]] = self.new(hash) + end + + # @param other [self, Integer] + # @return [Boolean] whether the given other is defined as a self + def exists?(other) + return false if other.nil? + validate other => [self, Integer] + other = other.id if other.is_a?(self) + return !self::DATA[other].nil? + end + + # @param other [self, Integer] + # @return [self] + def get(other) + validate other => [self, Integer] + return other if other.is_a?(self) + raise "Unknown ID #{other}." unless self::DATA.has_key?(other) + return self::DATA[other] + end + + def try_get(other) + return nil if other.nil? + validate other => [self, Integer] + return other if other.is_a?(self) + return (self::DATA.has_key?(other)) ? self::DATA[other] : nil + end + + # Returns the array of keys for the data. + # @return [Array] + def keys + return self::DATA.keys + end + + # Yields all data in numberical order. + def each + keys = self::DATA.keys.sort + keys.each { |key| yield self::DATA[key] } + end + + def load + const_set(:DATA, load_data("Data/#{self::DATA_FILENAME}")) + end + + def save + save_data(self::DATA, "Data/#{self::DATA_FILENAME}") + end + end + + #============================================================================= + # A mixin module for data classes which provides common instance methods + # (called by thing.method) that analyse the data of a particular thing which + # the instance represents. + #============================================================================= + module InstanceMethods + # @param other [Symbol, self.class, String, Integer] + # @return [Boolean] whether other represents the same thing as this thing + def ==(other) + return false if other.nil? + if other.is_a?(Symbol) + return @id == other + elsif other.is_a?(self.class) + return @id == other.id + elsif other.is_a?(String) + return @id_number == other.to_sym + elsif other.is_a?(Integer) + return @id_number == other + end + return false + end + end + + #============================================================================= + # A bulk loader method for all data stored in .dat files in the Data folder. + #============================================================================= + def self.load_all + Type.load + Ability.load + Move.load + Item.load + BerryPlant.load + Species.load + Ribbon.load + Encounter.load + EncounterModern.load + EncounterRandom.load + TrainerType.load + Trainer.load + TrainerModern.load + TrainerExpert.load + Metadata.load + MapMetadata.load + end +end diff --git a/Data/Scripts/010_Data/001_Hardcoded data/001_GrowthRate.rb b/Data/Scripts/010_Data/001_Hardcoded data/001_GrowthRate.rb new file mode 100644 index 000000000..9df096a61 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/001_GrowthRate.rb @@ -0,0 +1,190 @@ +module GameData + class GrowthRate + attr_reader :id + attr_reader :real_name + attr_reader :exp_values + attr_reader :exp_formula + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + # Calculates the maximum level a Pokémon can attain. This can vary during a + # game, and here is where you would make it do so. Note that this method is + # called by the Compiler, which happens before anything (e.g. Game Switches/ + # Variables, the player's data) is loaded, so code in this method should + # check whether the needed variables exist before using them; if they don't, + # this method should return the maximum possible level ever. + # @return [Integer] the maximum level attainable by a Pokémon + def self.max_level + return Settings::MAXIMUM_LEVEL + end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + @exp_values = hash[:exp_values] + @exp_formula = hash[:exp_formula] + end + + # @return [String] the translated name of this growth rate + def name + return _INTL(@real_name) + end + + # @param level [Integer] a level number + # @return [Integer] the minimum Exp needed to be at the given level + def minimum_exp_for_level(level) + return ArgumentError.new("Level #{level} is invalid.") if !level || level <= 0 + level = [level, GrowthRate.max_level].min + return @exp_values[level] if level < @exp_values.length + raise "No Exp formula is defined for growth rate #{name}" if !@exp_formula + return @exp_formula.call(level) + end + + # @return [Integer] the maximum Exp a Pokémon with this growth rate can have + def maximum_exp + return minimum_exp_for_level(GrowthRate.max_level) + end + + # @param exp1 [Integer] an Exp amount + # @param exp2 [Integer] an Exp amount + # @return [Integer] the sum of the two given Exp amounts + def add_exp(exp1, exp2) + return (exp1 + exp2).clamp(0, maximum_exp) + end + + # @param exp [Integer] an Exp amount + # @return [Integer] the level of a Pokémon that has the given Exp amount + def level_from_exp(exp) + return ArgumentError.new("Exp amount #{level} is invalid.") if !exp || exp < 0 + max = GrowthRate.max_level + return max if exp >= maximum_exp + for level in 1..max + return level - 1 if exp < minimum_exp_for_level(level) + end + return max + end + end +end + +#=============================================================================== + +GameData::GrowthRate.register({ + :id => :Medium, # Also known as Medium Fast + :name => _INTL("Medium"), + :exp_values => [-1, + 0, 8, 27, 64, 125, 216, 343, 512, 729, 1000, + 1331, 1728, 2197, 2744, 3375, 4096, 4913, 5832, 6859, 8000, + 9261, 10648, 12167, 13824, 15625, 17576, 19683, 21952, 24389, 27000, + 29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000, + 68921, 74088, 79507, 85184, 91125, 97336, 103823, 110592, 117649, 125000, + 132651, 140608, 148877, 157464, 166375, 175616, 185193, 195112, 205379, 216000, + 226981, 238328, 250047, 262144, 274625, 287496, 300763, 314432, 328509, 343000, + 357911, 373248, 389017, 405224, 421875, 438976, 456533, 474552, 493039, 512000, + 531441, 551368, 571787, 592704, 614125, 636056, 658503, 681472, 704969, 729000, + 753571, 778688, 804357, 830584, 857375, 884736, 912673, 941192, 970299, 1000000], + :exp_formula => proc { |level| next level ** 3 } +}) + +# Erratic (600000): +# For levels 0-50: n**3 * (100 - n) / 50 +# For levels 51-68: n**3 * (150 - n) / 100 +# For levels 69-98: n**3 * 1.274 - (n / 150) - p(n mod 3) +# where p(x) = array(0.000, 0.008, 0.014)[x] +# For levels 99-100: n**3 * (160 - n) / 100 +GameData::GrowthRate.register({ + :id => :Erratic, + :name => _INTL("Erratic"), + :exp_values => [-1, + 0, 15, 52, 122, 237, 406, 637, 942, 1326, 1800, + 2369, 3041, 3822, 4719, 5737, 6881, 8155, 9564, 11111, 12800, + 14632, 16610, 18737, 21012, 23437, 26012, 28737, 31610, 34632, 37800, + 41111, 44564, 48155, 51881, 55737, 59719, 63822, 68041, 72369, 76800, + 81326, 85942, 90637, 95406, 100237, 105122, 110052, 115015, 120001, 125000, + 131324, 137795, 144410, 151165, 158056, 165079, 172229, 179503, 186894, 194400, + 202013, 209728, 217540, 225443, 233431, 241496, 249633, 257834, 267406, 276458, + 286328, 296358, 305767, 316074, 326531, 336255, 346965, 357812, 367807, 378880, + 390077, 400293, 411686, 423190, 433572, 445239, 457001, 467489, 479378, 491346, + 501878, 513934, 526049, 536557, 548720, 560922, 571333, 583539, 591882, 600000], + :exp_formula => proc { |level| next (level ** 4) * 3 / 500 } +}) + +# Fluctuating (1640000): +# For levels 0-15 : n**3 * (24 + ((n + 1) / 3)) / 50 +# For levels 16-35: n**3 * (14 + n) / 50 +# For levels 36-100: n**3 * (32 + (n / 2)) / 50 +GameData::GrowthRate.register({ + :id => :Fluctuating, + :name => _INTL("Fluctuating"), + :exp_values => [-1, + 0, 4, 13, 32, 65, 112, 178, 276, 393, 540, + 745, 967, 1230, 1591, 1957, 2457, 3046, 3732, 4526, 5440, + 6482, 7666, 9003, 10506, 12187, 14060, 16140, 18439, 20974, 23760, + 26811, 30146, 33780, 37731, 42017, 46656, 50653, 55969, 60505, 66560, + 71677, 78533, 84277, 91998, 98415, 107069, 114205, 123863, 131766, 142500, + 151222, 163105, 172697, 185807, 196322, 210739, 222231, 238036, 250562, 267840, + 281456, 300293, 315059, 335544, 351520, 373744, 390991, 415050, 433631, 459620, + 479600, 507617, 529063, 559209, 582187, 614566, 639146, 673863, 700115, 737280, + 765275, 804997, 834809, 877201, 908905, 954084, 987754, 1035837, 1071552, 1122660, + 1160499, 1214753, 1254796, 1312322, 1354652, 1415577, 1460276, 1524731, 1571884, 1640000], + :exp_formula => proc { |level| + rate = [82 - (level - 100) / 2.0, 40].max + next (level ** 4) * rate / 5000 + } +}) + +GameData::GrowthRate.register({ + :id => :Parabolic, # Also known as Medium Slow + :name => _INTL("Parabolic"), + :exp_values => [-1, + 0, 9, 57, 96, 135, 179, 236, 314, 419, 560, + 742, 973, 1261, 1612, 2035, 2535, 3120, 3798, 4575, 5460, + 6458, 7577, 8825, 10208, 11735, 13411, 15244, 17242, 19411, 21760, + 24294, 27021, 29949, 33084, 36435, 40007, 43808, 47846, 52127, 56660, + 61450, 66505, 71833, 77440, 83335, 89523, 96012, 102810, 109923, 117360, + 125126, 133229, 141677, 150476, 159635, 169159, 179056, 189334, 199999, 211060, + 222522, 234393, 246681, 259392, 272535, 286115, 300140, 314618, 329555, 344960, + 360838, 377197, 394045, 411388, 429235, 447591, 466464, 485862, 505791, 526260, + 547274, 568841, 590969, 613664, 636935, 660787, 685228, 710266, 735907, 762160, + 789030, 816525, 844653, 873420, 902835, 932903, 963632, 995030, 1027103, 1059860], + :exp_formula => proc { |level| next ((level ** 3) * 6 / 5) - 15 * (level ** 2) + 100 * level - 140 } +}) + +GameData::GrowthRate.register({ + :id => :Fast, + :name => _INTL("Fast"), + :exp_values => [-1, + 0, 6, 21, 51, 100, 172, 274, 409, 583, 800, + 1064, 1382, 1757, 2195, 2700, 3276, 3930, 4665, 5487, 6400, + 7408, 8518, 9733, 11059, 12500, 14060, 15746, 17561, 19511, 21600, + 23832, 26214, 28749, 31443, 34300, 37324, 40522, 43897, 47455, 51200, + 55136, 59270, 63605, 68147, 72900, 77868, 83058, 88473, 94119, 100000, + 106120, 112486, 119101, 125971, 133100, 140492, 148154, 156089, 164303, 172800, + 181584, 190662, 200037, 209715, 219700, 229996, 240610, 251545, 262807, 274400, + 286328, 298598, 311213, 324179, 337500, 351180, 365226, 379641, 394431, 409600, + 425152, 441094, 457429, 474163, 491300, 508844, 526802, 545177, 563975, 583200, + 602856, 622950, 643485, 664467, 685900, 707788, 730138, 752953, 776239, 800000], + :exp_formula => proc { |level| (level ** 3) * 4 / 5 } +}) + +GameData::GrowthRate.register({ + :id => :Slow, + :name => _INTL("Slow"), + :exp_values => [-1, + 0, 10, 33, 80, 156, 270, 428, 640, 911, 1250, + 1663, 2160, 2746, 3430, 4218, 5120, 6141, 7290, 8573, 10000, + 11576, 13310, 15208, 17280, 19531, 21970, 24603, 27440, 30486, 33750, + 37238, 40960, 44921, 49130, 53593, 58320, 63316, 68590, 74148, 80000, + 86151, 92610, 99383, 106480, 113906, 121670, 129778, 138240, 147061, 156250, + 165813, 175760, 186096, 196830, 207968, 219520, 231491, 243890, 256723, 270000, + 283726, 297910, 312558, 327680, 343281, 359370, 375953, 393040, 410636, 428750, + 447388, 466560, 486271, 506530, 527343, 548720, 570666, 593190, 616298, 640000, + 664301, 689210, 714733, 740880, 767656, 795070, 823128, 851840, 881211, 911250, + 941963, 973360, 1005446, 1038230, 1071718, 1105920, 1140841, 1176490, 1212873, 1250000], + :exp_formula => proc { |level| (level ** 3) * 5 / 4 } +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/002_GenderRatio.rb b/Data/Scripts/010_Data/001_Hardcoded data/002_GenderRatio.rb new file mode 100644 index 000000000..b2d75ff50 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/002_GenderRatio.rb @@ -0,0 +1,77 @@ +# If a Pokémon's gender ratio is none of :AlwaysMale, :AlwaysFemale or +# :Genderless, then it will choose a random number between 0 and 255 inclusive, +# and compare it to the @female_chance. If the random number is lower than this +# chance, it will be female; otherwise, it will be male. +module GameData + class GenderRatio + attr_reader :id + attr_reader :real_name + attr_reader :female_chance + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + @female_chance = hash[:female_chance] + end + + # @return [String] the translated name of this gender ratio + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::GenderRatio.register({ + :id => :AlwaysMale, + :name => _INTL("Always Male") +}) + +GameData::GenderRatio.register({ + :id => :AlwaysFemale, + :name => _INTL("Always Female") +}) + +GameData::GenderRatio.register({ + :id => :Genderless, + :name => _INTL("Genderless") +}) + +GameData::GenderRatio.register({ + :id => :FemaleOneEighth, + :name => _INTL("Female One Eighth"), + :female_chance => 32 +}) + +GameData::GenderRatio.register({ + :id => :Female25Percent, + :name => _INTL("Female 25 Percent"), + :female_chance => 64 +}) + +GameData::GenderRatio.register({ + :id => :Female50Percent, + :name => _INTL("Female 50 Percent"), + :female_chance => 128 +}) + +GameData::GenderRatio.register({ + :id => :Female75Percent, + :name => _INTL("Female 75 Percent"), + :female_chance => 192 +}) + +GameData::GenderRatio.register({ + :id => :FemaleSevenEighths, + :name => _INTL("Female Seven Eighths"), + :female_chance => 224 +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/003_EggGroup.rb b/Data/Scripts/010_Data/001_Hardcoded data/003_EggGroup.rb new file mode 100644 index 000000000..496116bdc --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/003_EggGroup.rb @@ -0,0 +1,102 @@ +module GameData + class EggGroup + attr_reader :id + attr_reader :real_name + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + end + + # @return [String] the translated name of this egg group + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::EggGroup.register({ + :id => :Undiscovered, + :name => _INTL("Undiscovered") + }) + +GameData::EggGroup.register({ + :id => :Monster, + :name => _INTL("Monster") + }) + +GameData::EggGroup.register({ + :id => :Water1, + :name => _INTL("Water 1") + }) + +GameData::EggGroup.register({ + :id => :Bug, + :name => _INTL("Bug") + }) + +GameData::EggGroup.register({ + :id => :Flying, + :name => _INTL("Flying") + }) + +GameData::EggGroup.register({ + :id => :Field, + :name => _INTL("Field") + }) + +GameData::EggGroup.register({ + :id => :Fairy, + :name => _INTL("Fairy") + }) + +GameData::EggGroup.register({ + :id => :Grass, + :name => _INTL("Grass") + }) + +GameData::EggGroup.register({ + :id => :Humanlike, + :name => _INTL("Humanlike") + }) + +GameData::EggGroup.register({ + :id => :Water3, + :name => _INTL("Water 3") + }) + +GameData::EggGroup.register({ + :id => :Mineral, + :name => _INTL("Mineral") + }) + +GameData::EggGroup.register({ + :id => :Amorphous, + :name => _INTL("Amorphous") + }) + +GameData::EggGroup.register({ + :id => :Water2, + :name => _INTL("Water 2") + }) + +GameData::EggGroup.register({ + :id => :Ditto, + :name => _INTL("Ditto") + }) + +GameData::EggGroup.register({ + :id => :Dragon, + :name => _INTL("Dragon") + }) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/004_BodyShape.rb b/Data/Scripts/010_Data/001_Hardcoded data/004_BodyShape.rb new file mode 100644 index 000000000..ff3b4465f --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/004_BodyShape.rb @@ -0,0 +1,117 @@ +# NOTE: The id_number is only used to determine the order that body shapes are +# listed in the Pokédex search screen. Number 0 (:None) is ignored; they +# start with shape 1. +# "Graphics/Pictures/Pokedex/icon_shapes.png" contains icons for these +# shapes. +module GameData + class BodyShape + attr_reader :id + attr_reader :id_number + attr_reader :real_name + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + end + + # @return [String] the translated name of this body shape + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::BodyShape.register({ + :id => :Head, + :id_number => 1, + :name => _INTL("Head") +}) + +GameData::BodyShape.register({ + :id => :Serpentine, + :id_number => 2, + :name => _INTL("Serpentine") +}) + +GameData::BodyShape.register({ + :id => :Finned, + :id_number => 3, + :name => _INTL("Finned") +}) + +GameData::BodyShape.register({ + :id => :HeadArms, + :id_number => 4, + :name => _INTL("Head and arms") +}) + +GameData::BodyShape.register({ + :id => :HeadBase, + :id_number => 5, + :name => _INTL("Head and base") +}) + +GameData::BodyShape.register({ + :id => :BipedalTail, + :id_number => 6, + :name => _INTL("Bipedal with tail") +}) + +GameData::BodyShape.register({ + :id => :HeadLegs, + :id_number => 7, + :name => _INTL("Head and legs") +}) + +GameData::BodyShape.register({ + :id => :Quadruped, + :id_number => 8, + :name => _INTL("Quadruped") +}) + +GameData::BodyShape.register({ + :id => :Winged, + :id_number => 9, + :name => _INTL("Winged") +}) + +GameData::BodyShape.register({ + :id => :Multiped, + :id_number => 10, + :name => _INTL("Multiped") +}) + +GameData::BodyShape.register({ + :id => :MultiBody, + :id_number => 11, + :name => _INTL("Multi Body") +}) + +GameData::BodyShape.register({ + :id => :Bipedal, + :id_number => 12, + :name => _INTL("Bipedal") +}) + +GameData::BodyShape.register({ + :id => :MultiWinged, + :id_number => 13, + :name => _INTL("Multi Winged") +}) + +GameData::BodyShape.register({ + :id => :Insectoid, + :id_number => 14, + :name => _INTL("Insectoid") +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/005_BodyColor.rb b/Data/Scripts/010_Data/001_Hardcoded data/005_BodyColor.rb new file mode 100644 index 000000000..542c723be --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/005_BodyColor.rb @@ -0,0 +1,90 @@ +# NOTE: The id_number is only used to determine the order that body colors are +# listed in the Pokédex search screen. +module GameData + class BodyColor + attr_reader :id + attr_reader :id_number + attr_reader :real_name + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + end + + # @return [String] the translated name of this body color + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::BodyColor.register({ + :id => :Red, + :id_number => 0, + :name => _INTL("Red") +}) + +GameData::BodyColor.register({ + :id => :Blue, + :id_number => 1, + :name => _INTL("Blue") +}) + +GameData::BodyColor.register({ + :id => :Yellow, + :id_number => 2, + :name => _INTL("Yellow") +}) + +GameData::BodyColor.register({ + :id => :Green, + :id_number => 3, + :name => _INTL("Green") +}) + +GameData::BodyColor.register({ + :id => :Black, + :id_number => 4, + :name => _INTL("Black") +}) + +GameData::BodyColor.register({ + :id => :Brown, + :id_number => 5, + :name => _INTL("Brown") +}) + +GameData::BodyColor.register({ + :id => :Purple, + :id_number => 6, + :name => _INTL("Purple") +}) + +GameData::BodyColor.register({ + :id => :Gray, + :id_number => 7, + :name => _INTL("Gray") +}) + +GameData::BodyColor.register({ + :id => :White, + :id_number => 8, + :name => _INTL("White") +}) + +GameData::BodyColor.register({ + :id => :Pink, + :id_number => 9, + :name => _INTL("Pink") +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/006_Habitat.rb b/Data/Scripts/010_Data/001_Hardcoded data/006_Habitat.rb new file mode 100644 index 000000000..cfe1521c8 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/006_Habitat.rb @@ -0,0 +1,76 @@ +module GameData + class Habitat + attr_reader :id + attr_reader :real_name + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + end + + # @return [String] the translated name of this habitat + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::Habitat.register({ + :id => :None, + :name => _INTL("None") +}) + +GameData::Habitat.register({ + :id => :Grassland, + :name => _INTL("Grassland") +}) + +GameData::Habitat.register({ + :id => :Forest, + :name => _INTL("Forest") +}) + +GameData::Habitat.register({ + :id => :WatersEdge, + :name => _INTL("Water's Edge") +}) + +GameData::Habitat.register({ + :id => :Sea, + :name => _INTL("Sea") +}) + +GameData::Habitat.register({ + :id => :Cave, + :name => _INTL("Cave") +}) + +GameData::Habitat.register({ + :id => :Mountain, + :name => _INTL("Mountain") +}) + +GameData::Habitat.register({ + :id => :RoughTerrain, + :name => _INTL("Rough Terrain") +}) + +GameData::Habitat.register({ + :id => :Urban, + :name => _INTL("Urban") +}) + +GameData::Habitat.register({ + :id => :Rare, + :name => _INTL("Rare") +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/007_Evolution.rb b/Data/Scripts/010_Data/001_Hardcoded data/007_Evolution.rb new file mode 100644 index 000000000..896845700 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/007_Evolution.rb @@ -0,0 +1,599 @@ +module GameData + class Evolution + attr_reader :id + attr_reader :real_name + attr_reader :parameter + attr_reader :minimum_level # 0 means parameter is the minimum level + attr_reader :level_up_proc + attr_reader :use_item_proc + attr_reader :on_trade_proc + attr_reader :after_evolution_proc + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:id].to_s || "Unnamed" + @parameter = hash[:parameter] + @minimum_level = hash[:minimum_level] || 0 + @level_up_proc = hash[:level_up_proc] + @use_item_proc = hash[:use_item_proc] + @on_trade_proc = hash[:on_trade_proc] + @after_evolution_proc = hash[:after_evolution_proc] + end + + def call_level_up(*args) + return (@level_up_proc) ? @level_up_proc.call(*args) : nil + end + + def call_use_item(*args) + return (@use_item_proc) ? @use_item_proc.call(*args) : nil + end + + def call_on_trade(*args) + return (@on_trade_proc) ? @on_trade_proc.call(*args) : nil + end + + def call_after_evolution(*args) + @after_evolution_proc.call(*args) if @after_evolution_proc + end + end +end + +#=============================================================================== + +GameData::Evolution.register({ + :id => :None +}) + +GameData::Evolution.register({ + :id => :Level, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter + } +}) + +GameData::Evolution.register({ + :id => :LevelMale, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && pkmn.male? + } +}) + +GameData::Evolution.register({ + :id => :LevelFemale, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && pkmn.female? + } +}) + +GameData::Evolution.register({ + :id => :LevelDay, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && PBDayNight.isDay? + } +}) + +GameData::Evolution.register({ + :id => :LevelNight, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && PBDayNight.isNight? + } +}) + +GameData::Evolution.register({ + :id => :LevelMorning, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && PBDayNight.isMorning? + } +}) + +GameData::Evolution.register({ + :id => :LevelAfternoon, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && PBDayNight.isAfternoon? + } +}) + +GameData::Evolution.register({ + :id => :LevelEvening, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && PBDayNight.isEvening? + } +}) + +GameData::Evolution.register({ + :id => :LevelNoWeather, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $game_screen && $game_screen.weather_type == :None + } +}) + +GameData::Evolution.register({ + :id => :LevelSun, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $game_screen && + GameData::Weather.get($game_screen.weather_type).category == :Sun + } +}) + +GameData::Evolution.register({ + :id => :LevelRain, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $game_screen && + [:Rain, :Fog].include?(GameData::Weather.get($game_screen.weather_type).category) + } +}) + +GameData::Evolution.register({ + :id => :LevelSnow, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $game_screen && + GameData::Weather.get($game_screen.weather_type).category == :Hail + } +}) + +GameData::Evolution.register({ + :id => :LevelSandstorm, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $game_screen && + GameData::Weather.get($game_screen.weather_type).category == :Sandstorm + } +}) + +GameData::Evolution.register({ + :id => :LevelCycling, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $PokemonGlobal && $PokemonGlobal.bicycle + } +}) + +GameData::Evolution.register({ + :id => :LevelSurfing, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $PokemonGlobal && $PokemonGlobal.surfing + } +}) + +GameData::Evolution.register({ + :id => :LevelDiving, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $PokemonGlobal && $PokemonGlobal.diving + } +}) + +GameData::Evolution.register({ + :id => :LevelDarkness, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + map_metadata = GameData::MapMetadata.try_get($game_map.map_id) + next pkmn.level >= parameter && map_metadata && map_metadata.dark_map + } +}) + +GameData::Evolution.register({ + :id => :LevelDarkInParty, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && $Trainer.has_pokemon_of_type?(:DARK) + } +}) + +GameData::Evolution.register({ + :id => :AttackGreater, # Hitmonlee + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && pkmn.attack > pkmn.defense + } +}) + +GameData::Evolution.register({ + :id => :AtkDefEqual, # Hitmontop + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && pkmn.attack == pkmn.defense + } +}) + +GameData::Evolution.register({ + :id => :DefenseGreater, # Hitmonchan + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && pkmn.attack < pkmn.defense + } +}) + +GameData::Evolution.register({ + :id => :Silcoon, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && (((pkmn.personalID >> 16) & 0xFFFF) % 10) < 5 + } +}) + +GameData::Evolution.register({ + :id => :Cascoon, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter && (((pkmn.personalID >> 16) & 0xFFFF) % 10) >= 5 + } +}) + +GameData::Evolution.register({ + :id => :Ninjask, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next pkmn.level >= parameter + } +}) + +GameData::Evolution.register({ + :id => :Shedinja, + :parameter => Integer, + :level_up_proc => proc { |pkmn, parameter| + next false # This is a dummy proc and shouldn't next true + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if $Trainer.party_full? + next false if !$PokemonBag.pbHasItem?(:POKEBALL) + PokemonEvolutionScene.pbDuplicatePokemon(pkmn, new_species) + $PokemonBag.pbDeleteItem(:POKEBALL) + next true + } +}) + +GameData::Evolution.register({ + :id => :Happiness, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.happiness >= 220 + } +}) + +GameData::Evolution.register({ + :id => :HappinessMale, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.happiness >= 220 && pkmn.male? + } +}) + +GameData::Evolution.register({ + :id => :HappinessFemale, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.happiness >= 220 && pkmn.female? + } +}) + +GameData::Evolution.register({ + :id => :HappinessDay, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.happiness >= 220 && PBDayNight.isDay? + } +}) + +GameData::Evolution.register({ + :id => :HappinessNight, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.happiness >= 220 && PBDayNight.isNight? + } +}) + +GameData::Evolution.register({ + :id => :HappinessMove, + :parameter => :Move, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + if pkmn.happiness >= 220 + next pkmn.moves.any? { |m| m && m.id == parameter } + end + } +}) + +GameData::Evolution.register({ + :id => :HappinessMoveType, + :parameter => :Type, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + if pkmn.happiness >= 220 + next pkmn.moves.any? { |m| m && m.id > 0 && m.type == parameter } + end + } +}) + +GameData::Evolution.register({ + :id => :HappinessHoldItem, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter && pkmn.happiness >= 220 + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :MaxHappiness, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.happiness == 255 + } +}) + +GameData::Evolution.register({ + :id => :Beauty, # Feebas + :parameter => Integer, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.beauty >= parameter + } +}) + +GameData::Evolution.register({ + :id => :HoldItem, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :HoldItemMale, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter && pkmn.male? + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :HoldItemFemale, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter && pkmn.female? + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :DayHoldItem, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter && PBDayNight.isDay? + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :NightHoldItem, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter && PBDayNight.isNight? + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :HoldItemHappiness, + :parameter => :Item, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.item == parameter && pkmn.happiness >= 220 + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :HasMove, + :parameter => :Move, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.moves.any? { |m| m && m.id == parameter } + } +}) + +GameData::Evolution.register({ + :id => :HasMoveType, + :parameter => :Type, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next pkmn.moves.any? { |m| m && m.type == parameter } + } +}) + +GameData::Evolution.register({ + :id => :HasInParty, + :parameter => :Species, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next $Trainer.has_species?(parameter) + } +}) + +GameData::Evolution.register({ + :id => :Location, + :parameter => Integer, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + next $game_map.map_id == parameter + } +}) + +GameData::Evolution.register({ + :id => :Region, + :parameter => Integer, + :minimum_level => 1, # Needs any level up + :level_up_proc => proc { |pkmn, parameter| + map_metadata = GameData::MapMetadata.try_get($game_map.map_id) + next map_metadata && map_metadata.town_map_position && + map_metadata.town_map_position[0] == parameter + } +}) + +#=============================================================================== +# Evolution methods that trigger when using an item on the Pokémon +#=============================================================================== +GameData::Evolution.register({ + :id => :Item, + :parameter => :Item, + :use_item_proc => proc { |pkmn, parameter, item| + next item == parameter + } +}) + +GameData::Evolution.register({ + :id => :ItemMale, + :parameter => :Item, + :use_item_proc => proc { |pkmn, parameter, item| + next item == parameter && pkmn.male? + } +}) + +GameData::Evolution.register({ + :id => :ItemFemale, + :parameter => :Item, + :use_item_proc => proc { |pkmn, parameter, item| + next item == parameter && pkmn.female? + } +}) + +GameData::Evolution.register({ + :id => :ItemDay, + :parameter => :Item, + :use_item_proc => proc { |pkmn, parameter, item| + next item == parameter && PBDayNight.isDay? + } +}) + +GameData::Evolution.register({ + :id => :ItemNight, + :parameter => :Item, + :use_item_proc => proc { |pkmn, parameter, item| + next item == parameter && PBDayNight.isNight? + } +}) + +GameData::Evolution.register({ + :id => :ItemHappiness, + :parameter => :Item, + :use_item_proc => proc { |pkmn, parameter, item| + next item == parameter && pkmn.happiness >= 220 + } +}) + +#=============================================================================== +# Evolution methods that trigger when the Pokémon is obtained in a trade +#=============================================================================== +GameData::Evolution.register({ + :id => :Trade, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next true + } +}) + +GameData::Evolution.register({ + :id => :TradeMale, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next pkmn.male? + } +}) + +GameData::Evolution.register({ + :id => :TradeFemale, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next pkmn.female? + } +}) + +GameData::Evolution.register({ + :id => :TradeDay, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next PBDayNight.isDay? + } +}) + +GameData::Evolution.register({ + :id => :TradeNight, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next PBDayNight.isNight? + } +}) + +GameData::Evolution.register({ + :id => :TradeItem, + :parameter => :Item, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next pkmn.item == parameter + }, + :after_evolution_proc => proc { |pkmn, new_species, parameter, evo_species| + next false if evo_species != new_species || !pkmn.hasItem?(parameter) + pkmn.item = nil # Item is now consumed + next true + } +}) + +GameData::Evolution.register({ + :id => :TradeSpecies, + :parameter => :Species, + :on_trade_proc => proc { |pkmn, parameter, other_pkmn| + next pkmn.species == parameter && !other_pkmn.hasItem?(:EVERSTONE) + } +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/008_Stat.rb b/Data/Scripts/010_Data/001_Hardcoded data/008_Stat.rb new file mode 100644 index 000000000..07e6e4d80 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/008_Stat.rb @@ -0,0 +1,131 @@ +# The id_number value determines which order the stats are iterated through by +# the "each" methods. +# The pbs_order value determines the order in which the stats are written in +# several PBS files, where base stats/IVs/EVs/EV yields are defined. Only stats +# which are yielded by the "each_main" method can have stat numbers defined in +# those places. The values of pbs_order defined below should start with 0 and +# increase without skipping any numbers. +module GameData + class Stat + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :real_name_brief + attr_reader :type + attr_reader :pbs_order + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + # These stats are defined in PBS files, and should have the :pbs_order + # property. + def self.each_main + self.each { |s| yield s if [:main, :main_battle].include?(s.type) } + end + + def self.each_main_battle + self.each { |s| yield s if [:main_battle].include?(s.type) } + end + + # These stats have associated stat stages in battle. + def self.each_battle + self.each { |s| yield s if [:main_battle, :battle].include?(s.type) } + end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @real_name_brief = hash[:name_brief] || "None" + @type = hash[:type] || :none + @pbs_order = hash[:pbs_order] || -1 + end + + # @return [String] the translated name of this stat + def name + return _INTL(@real_name) + end + + # @return [String] the translated brief name of this stat + def name_brief + return _INTL(@real_name_brief) + end + end +end + +#=============================================================================== + +GameData::Stat.register({ + :id => :HP, + :id_number => 0, + :name => _INTL("HP"), + :name_brief => _INTL("HP"), + :type => :main, + :pbs_order => 0 +}) + +GameData::Stat.register({ + :id => :ATTACK, + :id_number => 1, + :name => _INTL("Attack"), + :name_brief => _INTL("Atk"), + :type => :main_battle, + :pbs_order => 1 +}) + +GameData::Stat.register({ + :id => :DEFENSE, + :id_number => 2, + :name => _INTL("Defense"), + :name_brief => _INTL("Def"), + :type => :main_battle, + :pbs_order => 2 +}) + +GameData::Stat.register({ + :id => :SPECIAL_ATTACK, + :id_number => 3, + :name => _INTL("Special Attack"), + :name_brief => _INTL("SpAtk"), + :type => :main_battle, + :pbs_order => 4 +}) + +GameData::Stat.register({ + :id => :SPECIAL_DEFENSE, + :id_number => 4, + :name => _INTL("Special Defense"), + :name_brief => _INTL("SpDef"), + :type => :main_battle, + :pbs_order => 5 +}) + +GameData::Stat.register({ + :id => :SPEED, + :id_number => 5, + :name => _INTL("Speed"), + :name_brief => _INTL("Spd"), + :type => :main_battle, + :pbs_order => 3 +}) + +GameData::Stat.register({ + :id => :ACCURACY, + :id_number => 6, + :name => _INTL("accuracy"), + :name_brief => _INTL("Acc"), + :type => :battle +}) + +GameData::Stat.register({ + :id => :EVASION, + :id_number => 7, + :name => _INTL("evasiveness"), + :name_brief => _INTL("Eva"), + :type => :battle +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/009_Nature.rb b/Data/Scripts/010_Data/001_Hardcoded data/009_Nature.rb new file mode 100644 index 000000000..f745ef21d --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/009_Nature.rb @@ -0,0 +1,200 @@ +module GameData + class Nature + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :stat_changes + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @stat_changes = hash[:stat_changes] || [] + end + + # @return [String] the translated name of this nature + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::Nature.register({ + :id => :HARDY, + :id_number => 0, + :name => _INTL("Hardy") +}) + +GameData::Nature.register({ + :id => :LONELY, + :id_number => 1, + :name => _INTL("Lonely"), + :stat_changes => [[:ATTACK, 10], [:DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :BRAVE, + :id_number => 2, + :name => _INTL("Brave"), + :stat_changes => [[:ATTACK, 10], [:SPEED, -10]] +}) + +GameData::Nature.register({ + :id => :ADAMANT, + :id_number => 3, + :name => _INTL("Adamant"), + :stat_changes => [[:ATTACK, 10], [:SPECIAL_ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :NAUGHTY, + :id_number => 4, + :name => _INTL("Naughty"), + :stat_changes => [[:ATTACK, 10], [:SPECIAL_DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :BOLD, + :id_number => 5, + :name => _INTL("Bold"), + :stat_changes => [[:DEFENSE, 10], [:ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :DOCILE, + :id_number => 6, + :name => _INTL("Docile") +}) + +GameData::Nature.register({ + :id => :RELAXED, + :id_number => 7, + :name => _INTL("Relaxed"), + :stat_changes => [[:DEFENSE, 10], [:SPEED, -10]] +}) + +GameData::Nature.register({ + :id => :IMPISH, + :id_number => 8, + :name => _INTL("Impish"), + :stat_changes => [[:DEFENSE, 10], [:SPECIAL_ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :LAX, + :id_number => 9, + :name => _INTL("Lax"), + :stat_changes => [[:DEFENSE, 10], [:SPECIAL_DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :TIMID, + :id_number => 10, + :name => _INTL("Timid"), + :stat_changes => [[:SPEED, 10], [:ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :HASTY, + :id_number => 11, + :name => _INTL("Hasty"), + :stat_changes => [[:SPEED, 10], [:DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :SERIOUS, + :id_number => 12, + :name => _INTL("Serious") +}) + +GameData::Nature.register({ + :id => :JOLLY, + :id_number => 13, + :name => _INTL("Jolly"), + :stat_changes => [[:SPEED, 10], [:SPECIAL_ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :NAIVE, + :id_number => 14, + :name => _INTL("Naive"), + :stat_changes => [[:SPEED, 10], [:SPECIAL_DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :MODEST, + :id_number => 15, + :name => _INTL("Modest"), + :stat_changes => [[:SPECIAL_ATTACK, 10], [:ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :MILD, + :id_number => 16, + :name => _INTL("Mild"), + :stat_changes => [[:SPECIAL_ATTACK, 10], [:DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :QUIET, + :id_number => 17, + :name => _INTL("Quiet"), + :stat_changes => [[:SPECIAL_ATTACK, 10], [:SPEED, -10]] +}) + +GameData::Nature.register({ + :id => :BASHFUL, + :id_number => 18, + :name => _INTL("Bashful") +}) + +GameData::Nature.register({ + :id => :RASH, + :id_number => 19, + :name => _INTL("Rash"), + :stat_changes => [[:SPECIAL_ATTACK, 10], [:SPECIAL_DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :CALM, + :id_number => 20, + :name => _INTL("Calm"), + :stat_changes => [[:SPECIAL_DEFENSE, 10], [:ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :GENTLE, + :id_number => 21, + :name => _INTL("Gentle"), + :stat_changes => [[:SPECIAL_DEFENSE, 10], [:DEFENSE, -10]] +}) + +GameData::Nature.register({ + :id => :SASSY, + :id_number => 22, + :name => _INTL("Sassy"), + :stat_changes => [[:SPECIAL_DEFENSE, 10], [:SPEED, -10]] +}) + +GameData::Nature.register({ + :id => :CAREFUL, + :id_number => 23, + :name => _INTL("Careful"), + :stat_changes => [[:SPECIAL_DEFENSE, 10], [:SPECIAL_ATTACK, -10]] +}) + +GameData::Nature.register({ + :id => :QUIRKY, + :id_number => 24, + :name => _INTL("Quirky") +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/010_Status.rb b/Data/Scripts/010_Data/001_Hardcoded data/010_Status.rb new file mode 100644 index 000000000..0b852bd04 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/010_Status.rb @@ -0,0 +1,79 @@ +# NOTE: The id_number is only used to determine the order of the status icons in +# the graphics containing them. Number 0 (:NONE) is ignored; they start +# with status 1. +# "Graphics/Pictures/statuses.png" also contains icons for being fainted +# and for having Pokérus, in that order, at the bottom of the graphic. +# "Graphics/Pictures/Battle/icon_statuses.png" also contains an icon for +# bad poisoning (toxic), at the bottom of the graphic. +# Both graphics automatically handle varying numbers of defined statuses. +module GameData + class Status + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :animation + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] + @real_name = hash[:name] || "Unnamed" + @animation = hash[:animation] + end + + # @return [String] the translated name of this status condition + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::Status.register({ + :id => :NONE, + :id_number => 0, + :name => _INTL("None") +}) + +GameData::Status.register({ + :id => :SLEEP, + :id_number => 1, + :name => _INTL("Sleep"), + :animation => "Sleep" +}) + +GameData::Status.register({ + :id => :POISON, + :id_number => 2, + :name => _INTL("Poison"), + :animation => "Poison" +}) + +GameData::Status.register({ + :id => :BURN, + :id_number => 3, + :name => _INTL("Burn"), + :animation => "Burn" +}) + +GameData::Status.register({ + :id => :PARALYSIS, + :id_number => 4, + :name => _INTL("Paralysis"), + :animation => "Paralysis" +}) + +GameData::Status.register({ + :id => :FROZEN, + :id_number => 5, + :name => _INTL("Frozen"), + :animation => "Frozen" +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/011_TerrainTag.rb b/Data/Scripts/010_Data/001_Hardcoded data/011_TerrainTag.rb new file mode 100644 index 000000000..e8ebdc944 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/011_TerrainTag.rb @@ -0,0 +1,302 @@ +module GameData + class TerrainTag + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :can_surf + attr_reader :waterfall # The main part only, not the crest + attr_reader :waterfall_crest + attr_reader :can_fish + attr_reader :can_dive + attr_reader :deep_bush + attr_reader :shows_grass_rustle + attr_reader :land_wild_encounters + attr_reader :double_wild_encounters + attr_reader :battle_environment + attr_reader :ledge + attr_reader :ice + attr_reader :bridge + attr_reader :waterCurrent + attr_reader :shows_reflections + attr_reader :must_walk + attr_reader :ignore_passability + + #oricorio + attr_reader :flowerRed + attr_reader :flowerPink + attr_reader :flowerYellow + attr_reader :flowerBlue + attr_reader :flower + attr_reader :trashcan + attr_reader :sharpedoObstacle + attr_reader :underwater #only visible when diving + + DATA = {} + + extend ClassMethods + include InstanceMethods + + # @param other [Symbol, self, String, Integer] + # @return [self] + def self.try_get(other) + return self.get(:None) if other.nil? + validate other => [Symbol, self, String, Integer] + return other if other.is_a?(self) + other = other.to_sym if other.is_a?(String) + return (self::DATA.has_key?(other)) ? self::DATA[other] : self.get(:None) + end + + def self.load; end + + def self.save; end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] + @real_name = hash[:id].to_s || "Unnamed" + @can_surf = hash[:can_surf] || false + @waterfall = hash[:waterfall] || false + @waterfall_crest = hash[:waterfall_crest] || false + @can_fish = hash[:can_fish] || false + @can_dive = hash[:can_dive] || false + @deep_bush = hash[:deep_bush] || false + @shows_grass_rustle = hash[:shows_grass_rustle] || false + @land_wild_encounters = hash[:land_wild_encounters] || false + @double_wild_encounters = hash[:double_wild_encounters] || false + @battle_environment = hash[:battle_environment] + @ledge = hash[:ledge] || false + @ice = hash[:ice] || false + @waterCurrent = hash[:waterCurrent] || false + @bridge = hash[:bridge] || false + @shows_reflections = hash[:shows_reflections] || false + @must_walk = hash[:must_walk] || false + @ignore_passability = hash[:ignore_passability] || false + @ignore_passability = hash[:ignore_passability] || false + + @flowerRed = hash[:flowerRed] || false + @flowerYellow = hash[:flowerYellow] || false + @flowerPink = hash[:flowerPink] || false + @flowerBlue = hash[:flowerBlue] || false + @flower = hash[:flower] || false + @trashcan = hash[:trashcan] || false + @sharpedoObstacle = hash[:sharpedoObstacle] || false + @underwater = hash[:underwater] || false + + end + + def can_surf_freely + return @can_surf && !@waterfall && !@waterfall_crest + end + end +end + +#=============================================================================== + +GameData::TerrainTag.register({ + :id => :None, + :id_number => 0 + }) + +GameData::TerrainTag.register({ + :id => :Ledge, + :id_number => 1, + :ledge => true + }) + +GameData::TerrainTag.register({ + :id => :Grass, + :id_number => 2, + :shows_grass_rustle => true, + :land_wild_encounters => true, + :battle_environment => :Grass + }) + +GameData::TerrainTag.register({ + :id => :Sand, + :id_number => 3, + :battle_environment => :Sand + }) + +GameData::TerrainTag.register({ + :id => :Rock, + :id_number => 15, + :battle_environment => :Rock + }) + +GameData::TerrainTag.register({ + :id => :DeepWater, + :id_number => 5, + :can_surf => true, + :can_fish => true, + :can_dive => true, + :battle_environment => :MovingWater + }) + +GameData::TerrainTag.register({ + :id => :WaterCurrent, + :id_number => 6, + :can_surf => true, + :can_fish => true, + :waterCurrent => true, + :battle_environment => :MovingWater + }) + + + +GameData::TerrainTag.register({ + :id => :Water, + :id_number => 7, + :can_surf => true, + :can_fish => true, + :battle_environment => :MovingWater + }) + +GameData::TerrainTag.register({ + :id => :Waterfall, + :id_number => 8, + :can_surf => true, + :waterfall => true + }) + +GameData::TerrainTag.register({ + :id => :WaterfallCrest, + :id_number => 9, + :can_surf => true, + :can_fish => true, + :waterfall_crest => true + }) + +GameData::TerrainTag.register({ + :id => :TallGrass, + :id_number => 10, + :deep_bush => true, + :land_wild_encounters => true, + :double_wild_encounters => true, + :battle_environment => :TallGrass, + :must_walk => true + }) + +GameData::TerrainTag.register({ + :id => :UnderwaterGrass, + :id_number => 11, + :underwater => true, + :land_wild_encounters => true + }) + +GameData::TerrainTag.register({ + :id => :Ice, + :id_number => 12, + :battle_environment => :Ice, + :ice => true, + :must_walk => true + }) + +GameData::TerrainTag.register({ + :id => :Neutral, + :id_number => 13, + :ignore_passability => true + }) + +# NOTE: This is referenced by ID in an Events.onStepTakenFieldMovement proc that +# adds soot to the Soot Sack if the player walks over one of these tiles. +GameData::TerrainTag.register({ + :id => :SootGrass, + :id_number => 14, + :shows_grass_rustle => true, + :land_wild_encounters => true, + :battle_environment => :Grass + }) + +GameData::TerrainTag.register({ + :id => :Bridge, + :id_number => 4, + :bridge => true + }) + +GameData::TerrainTag.register({ + :id => :Puddle, + :id_number => 16, + :battle_environment => :Puddle, + :shows_reflections => false + }) + +GameData::TerrainTag.register({ + :id => :FlowerRed, + :id_number => 17, + :flowerRed => true, + :flower => true + }) +GameData::TerrainTag.register({ + :id => :FlowerYellow, + :id_number => 18, + :flowerYellow => true, + :flower => true + }) +GameData::TerrainTag.register({ + :id => :FlowerPink, + :id_number => 19, + :flowerPink => true, + :flower => true + }) +GameData::TerrainTag.register({ + :id => :FlowerBlue, + :id_number => 20, + :flowerBlue => true, + :flower => true + }) +GameData::TerrainTag.register({ + :id => :FlowerOther, + :id_number => 21, + :flower => true + }) +GameData::TerrainTag.register({ + :id => :Trashcan, + :id_number => 22, + :trashcan => true + }) + +GameData::TerrainTag.register({ + :id => :SharpedoObstacle, + :id_number => 23, + :sharpedoObstacle => true + }) + +GameData::TerrainTag.register({ + :id => :Grass_alt1, + :id_number => 24, + :shows_grass_rustle => true, + :land_wild_encounters => true, + :battle_environment => :Grass + }) + +GameData::TerrainTag.register({ + :id => :Grass_alt2, + :id_number => 25, + :shows_grass_rustle => true, + :land_wild_encounters => true, + :battle_environment => :Grass + }) + +GameData::TerrainTag.register({ + :id => :Grass_alt3, + :id_number => 26, + :shows_grass_rustle => true, + :land_wild_encounters => true, + :battle_environment => :Grass + }) + +GameData::TerrainTag.register({ + :id => :StillWater, + :id_number => 27, + :can_surf => true, + :can_fish => true, + :battle_environment => :StillWater, + :shows_reflections => true + }) + +GameData::TerrainTag.register({ + :id => :Underwater, + :id_number => 28, + :battle_environment => :underwater, + :underwater => true, + }) \ No newline at end of file diff --git a/Data/Scripts/010_Data/001_Hardcoded data/012_Weather.rb b/Data/Scripts/010_Data/001_Hardcoded data/012_Weather.rb new file mode 100644 index 000000000..9276466d5 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/012_Weather.rb @@ -0,0 +1,194 @@ +# Category has the following effects: +# - Determines the in-battle weather. +# - Some abilities reduce the encounter rate in certain categories of weather. +# - Some evolution methods check the current weather's category. +# - The :Rain category treats the last listed particle graphic as a water splash rather +# than a raindrop, which behaves differently. +# - :Rain auto-waters berry plants. +# Delta values are per second. +# For the tone_proc, strength goes from 0 to RPG::Weather::MAX_SPRITES (60) and +# will typically be the maximum. +module GameData + class + Weather + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :category # :None, :Rain, :Hail, :Sandstorm, :Sun, :Fog + attr_reader :graphics # [[particle file names], [tile file names]] + attr_reader :particle_delta_x + attr_reader :particle_delta_y + attr_reader :particle_delta_opacity + attr_reader :tile_delta_x + attr_reader :tile_delta_y + attr_reader :tone_proc + attr_reader :fog_name + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] + @real_name = hash[:id].to_s || "Unnamed" + @category = hash[:category] || :None + @particle_delta_x = hash[:particle_delta_x] || 0 + @particle_delta_y = hash[:particle_delta_y] || 0 + @particle_delta_opacity = hash[:particle_delta_opacity] || 0 + @tile_delta_x = hash[:tile_delta_x] || 0 + @tile_delta_y = hash[:tile_delta_y] || 0 + @graphics = hash[:graphics] || [] + @tone_proc = hash[:tone_proc] + @fog_name = hash[:fog_name] + + end + + def has_particles? + return @graphics[0] && @graphics[0].length > 0 + end + + def has_tiles? + return @graphics[1] && @graphics[1].length > 0 + end + + def tone(strength) + return (@tone_proc) ? @tone_proc.call(strength) : Tone.new(0, 0, 0, 0) + end + end +end + +#=============================================================================== + +GameData::Weather.register({ + :id => :None, + :id_number => 0 # Must be 0 (preset RMXP weather) +}) + +GameData::Weather.register({ + :id => :Rain, + :id_number => 1, # Must be 1 (preset RMXP weather) + :category => :Rain, + :graphics => [["rain_1", "rain_2", "rain_3", "rain_4"]], # Last is splash + :particle_delta_x => -1200, + :particle_delta_y => 4800, + :tone_proc => proc { |strength| + next Tone.new(-strength * 3 / 4, -strength * 3 / 4, -strength * 3 / 4, 10) + } +}) + +# NOTE: This randomly flashes the screen in RPG::Weather#update. +GameData::Weather.register({ + :id => :Storm, + :id_number => 2, # Must be 2 (preset RMXP weather) + :category => :Rain, + :graphics => [["storm_1", "storm_2", "storm_3", "storm_4"]], # Last is splash + :particle_delta_x => -4800, + :particle_delta_y => 4800, + :tone_proc => proc { |strength| + next Tone.new(-strength * 3 / 2, -strength * 3 / 2, -strength * 3 / 2, 20) + } +}) + +# NOTE: This alters the movement of snow particles in RPG::Weather#update_sprite_position. +GameData::Weather.register({ + :id => :Snow, + :id_number => 3, # Must be 3 (preset RMXP weather) + :category => :Hail, + :graphics => [["hail_1", "hail_2", "hail_3"]], + :particle_delta_x => -240, + :particle_delta_y => 240, + :tone_proc => proc { |strength| + next Tone.new(strength / 2, strength / 2, strength / 2, 0) + } +}) + +GameData::Weather.register({ + :id => :Blizzard, + :id_number => 4, + :category => :Hail, + :graphics => [["blizzard_1", "blizzard_2", "blizzard_3", "blizzard_4"], ["blizzard_tile"]], + :particle_delta_x => -960, + :particle_delta_y => 240, + :tile_delta_x => -1440, + :tile_delta_y => 720, + :tone_proc => proc { |strength| + next Tone.new(strength * 3 / 4, strength * 3 / 4, strength * 3 / 4, 0) + } +}) + +GameData::Weather.register({ + :id => :Sandstorm, + :id_number => 5, + :category => :Sandstorm, + :graphics => [["sandstorm_1", "sandstorm_2", "sandstorm_3", "sandstorm_4"], ["sandstorm_tile"]], + :particle_delta_x => -1200, + :particle_delta_y => 640, + :tile_delta_x => -720, + :tile_delta_y => 360, + :tone_proc => proc { |strength| + next Tone.new(strength / 2, 0, -strength / 2, 0) + } +}) + +GameData::Weather.register({ + :id => :HeavyRain, + :id_number => 6, + :category => :Rain, + :graphics => [["storm_1", "storm_2", "storm_3", "storm_4"]], # Last is splash + :particle_delta_x => -4800, + :particle_delta_y => 4800, + :tone_proc => proc { |strength| + next Tone.new(-strength * 3 / 2, -strength * 3 / 2, -strength * 3 / 2, 20) + } +}) + +# NOTE: This alters the screen tone in RPG::Weather#update_screen_tone. +GameData::Weather.register({ + :id => :Sunny, + :id_number => 7, + :category => :Sun, + :tone_proc => proc { |strength| + next Tone.new(32, 32, 16, 0) + } +}) + +GameData::Weather.register({ + :id => :Fog, + :category => :Fog, + :id_number => 8, + :tile_delta_x => -2, + :tile_delta_y => 0, + :fog_name => "fog_tile" +}) + +GameData::Weather.register({ + :id => :Wind, + :category => :StrongWinds, + :id_number => 9, + :particle_delta_x => -1000, + :particle_delta_y => 0, + :graphics => [["wind1","wind2","wind3","windleaf1","windleaf2"], nil] +}) + +GameData::Weather.register({ + :id => :StrongWinds, + :category => :StrongWinds, + :id_number => 10, + :particle_delta_x => -1000, + :particle_delta_y => 0, + :graphics => [["wind1","wind2","wind3","windleaf1","windleaf2"], nil] +}) + +GameData::Weather.register({ + :id => :HarshSun, + :id_number => 11, + :category => :Sun, + :tone_proc => proc { |strength| + next Tone.new(64, 64, 32, 0) + } +}) \ No newline at end of file diff --git a/Data/Scripts/010_Data/001_Hardcoded data/013_EncounterType.rb b/Data/Scripts/010_Data/001_Hardcoded data/013_EncounterType.rb new file mode 100644 index 000000000..f9531ee68 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/013_EncounterType.rb @@ -0,0 +1,219 @@ +module GameData + + class EncounterType + attr_reader :id + attr_reader :real_name + attr_reader :type # :land, :cave, :water, :fishing, :contest, :none + attr_reader :trigger_chance + attr_reader :old_slots + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:id].to_s || "Unnamed" + @type = hash[:type] || :none + @trigger_chance = hash[:trigger_chance] || 0 + @old_slots = hash[:old_slots] + end + end +end + +#=============================================================================== + +GameData::EncounterType.register({ + :id => :Land, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :Land1, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] + }) + +GameData::EncounterType.register({ + :id => :Land2, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] + }) + +GameData::EncounterType.register({ + :id => :Land3, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] + }) + +GameData::EncounterType.register({ + :id => :LandDay, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :LandNight, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :LandMorning, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :LandAfternoon, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :LandEvening, + :type => :land, + :trigger_chance => 10, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :Cave, + :type => :cave, + :trigger_chance => 5, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :CaveDay, + :type => :cave, + :trigger_chance => 5, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :CaveNight, + :type => :cave, + :trigger_chance => 5, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :CaveMorning, + :type => :cave, + :trigger_chance => 5, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :CaveAfternoon, + :type => :cave, + :trigger_chance => 5, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :CaveEvening, + :type => :cave, + :trigger_chance => 5, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :Water, + :type => :water, + :trigger_chance => 2, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :WaterDay, + :type => :water, + :trigger_chance => 2, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :WaterNight, + :type => :water, + :trigger_chance => 2, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :WaterMorning, + :type => :water, + :trigger_chance => 2, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :WaterAfternoon, + :type => :water, + :trigger_chance => 2, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :WaterEvening, + :type => :water, + :trigger_chance => 2, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :OldRod, + :type => :fishing, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :GoodRod, + :type => :fishing, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :SuperRod, + :type => :fishing, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :RockSmash, + :type => :none, + :trigger_chance => 50, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :HeadbuttLow, + :type => :none, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :HeadbuttHigh, + :type => :none, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) + +GameData::EncounterType.register({ + :id => :BugContest, + :type => :contest, + :trigger_chance => 21, + :old_slots => [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/014_Environment.rb b/Data/Scripts/010_Data/001_Hardcoded data/014_Environment.rb new file mode 100644 index 000000000..c7923ac01 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/014_Environment.rb @@ -0,0 +1,131 @@ +module GameData + class Environment + attr_reader :id + attr_reader :real_name + attr_reader :battle_base + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + @battle_base = hash[:battle_base] + end + + # @return [String] the translated name of this environment + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::Environment.register({ + :id => :None, + :name => _INTL("None") +}) + +GameData::Environment.register({ + :id => :Grass, + :name => _INTL("Grass"), + :battle_base => "grass" +}) + +GameData::Environment.register({ + :id => :TallGrass, + :name => _INTL("Tall grass"), + :battle_base => "grass" +}) + +GameData::Environment.register({ + :id => :MovingWater, + :name => _INTL("Moving water"), + :battle_base => "water" +}) + +GameData::Environment.register({ + :id => :StillWater, + :name => _INTL("Still water"), + :battle_base => "water" +}) + +GameData::Environment.register({ + :id => :Puddle, + :name => _INTL("Puddle"), + :battle_basec => "puddle" +}) + +GameData::Environment.register({ + :id => :Underwater, + :name => _INTL("Underwater") +}) + +GameData::Environment.register({ + :id => :Cave, + :name => _INTL("Cave") +}) + +GameData::Environment.register({ + :id => :Rock, + :name => _INTL("Rock") +}) + +GameData::Environment.register({ + :id => :Sand, + :name => _INTL("Sand"), + :battle_base => "sand" +}) + +GameData::Environment.register({ + :id => :Forest, + :name => _INTL("Forest") +}) + +GameData::Environment.register({ + :id => :ForestGrass, + :name => _INTL("Forest grass"), + :battle_base => "grass" +}) + +GameData::Environment.register({ + :id => :Snow, + :name => _INTL("Snow") +}) + +GameData::Environment.register({ + :id => :Ice, + :name => _INTL("Ice"), + :battle_base => "ice" +}) + +GameData::Environment.register({ + :id => :Volcano, + :name => _INTL("Volcano") +}) + +GameData::Environment.register({ + :id => :Graveyard, + :name => _INTL("Graveyard") +}) + +GameData::Environment.register({ + :id => :Sky, + :name => _INTL("Sky") +}) + +GameData::Environment.register({ + :id => :Space, + :name => _INTL("Space") +}) + +GameData::Environment.register({ + :id => :UltraSpace, + :name => _INTL("Ultra Space") +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/015_BattleWeather.rb b/Data/Scripts/010_Data/001_Hardcoded data/015_BattleWeather.rb new file mode 100644 index 000000000..27e7eb5e3 --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/015_BattleWeather.rb @@ -0,0 +1,81 @@ +module GameData + class BattleWeather + attr_reader :id + attr_reader :real_name + attr_reader :animation + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + @animation = hash[:animation] + end + + # @return [String] the translated name of this battle weather + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::BattleWeather.register({ + :id => :None, + :name => _INTL("None") +}) + +GameData::BattleWeather.register({ + :id => :Sun, + :name => _INTL("Sun"), + :animation => "Sun" +}) + +GameData::BattleWeather.register({ + :id => :Rain, + :name => _INTL("Rain"), + :animation => "Rain" +}) + +GameData::BattleWeather.register({ + :id => :Sandstorm, + :name => _INTL("Sandstorm"), + :animation => "Sandstorm" +}) + +GameData::BattleWeather.register({ + :id => :Hail, + :name => _INTL("Hail"), + :animation => "Hail" +}) + +GameData::BattleWeather.register({ + :id => :HarshSun, + :name => _INTL("Harsh Sun"), + :animation => "HarshSun" +}) + +GameData::BattleWeather.register({ + :id => :HeavyRain, + :name => _INTL("Heavy Rain"), + :animation => "HeavyRain" +}) + +GameData::BattleWeather.register({ + :id => :StrongWinds, + :name => _INTL("Strong Winds"), + :animation => "StrongWinds" +}) + +GameData::BattleWeather.register({ + :id => :ShadowSky, + :name => _INTL("Shadow Sky"), + :animation => "ShadowSky" +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/016_BattleTerrain.rb b/Data/Scripts/010_Data/001_Hardcoded data/016_BattleTerrain.rb new file mode 100644 index 000000000..5a8f99c3f --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/016_BattleTerrain.rb @@ -0,0 +1,58 @@ +# These are in-battle terrain effects caused by moves like Electric Terrain. +module GameData + class BattleTerrain + attr_reader :id + attr_reader :real_name + attr_reader :animation + + DATA = {} + + extend ClassMethodsSymbols + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + @animation = hash[:animation] + end + + # @return [String] the translated name of this battle terrain + def name + return _INTL(@real_name) + end + end +end + +#=============================================================================== + +GameData::BattleTerrain.register({ + :id => :None, + :name => _INTL("None") +}) + +GameData::BattleTerrain.register({ + :id => :Electric, + :name => _INTL("Electric"), + :animation => "ElectricTerrain" +}) + +GameData::BattleTerrain.register({ + :id => :Grassy, + :name => _INTL("Grassy"), + :animation => "GrassyTerrain" +}) + +GameData::BattleTerrain.register({ + :id => :Misty, + :name => _INTL("Misty"), + :animation => "MistyTerrain" +}) + +GameData::BattleTerrain.register({ + :id => :Psychic, + :name => _INTL("Psychic"), + :animation => "PsychicTerrain" +}) diff --git a/Data/Scripts/010_Data/001_Hardcoded data/017_Target.rb b/Data/Scripts/010_Data/001_Hardcoded data/017_Target.rb new file mode 100644 index 000000000..1eea4e43f --- /dev/null +++ b/Data/Scripts/010_Data/001_Hardcoded data/017_Target.rb @@ -0,0 +1,194 @@ +# NOTE: If adding a new target, you will need to add code in several places to +# make them work properly: +# - def pbFindTargets +# - def pbMoveCanTarget? +# - def pbCreateTargetTexts +# - def pbFirstTarget +# - def pbTargetsMultiple? +module GameData + class Target + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :num_targets # 0, 1 or 2 (meaning 2+) + attr_reader :targets_foe # Is able to target one or more foes + attr_reader :targets_all # Crafty Shield can't protect from these moves + attr_reader :affects_foe_side # Pressure also affects these moves + attr_reader :long_range # Hits non-adjacent targets + + DATA = {} + + extend ClassMethods + include InstanceMethods + + def self.load; end + def self.save; end + + def initialize(hash) + @id = hash[:id] + @real_name = hash[:name] || "Unnamed" + @num_targets = hash[:num_targets] || 0 + @targets_foe = hash[:targets_foe] || false + @targets_all = hash[:targets_all] || false + @affects_foe_side = hash[:affects_foe_side] || false + @long_range = hash[:long_range] || false + end + + # @return [String] the translated name of this target + def name + return _INTL(@real_name) + end + + def can_choose_distant_target? + return @num_targets == 1 && @long_range + end + + def can_target_one_foe? + return @num_targets == 1 && @targets_foe + end + end +end + +#=============================================================================== + +# Bide, Counter, Metal Burst, Mirror Coat (calculate a target) +GameData::Target.register({ + :id => :None, + :id_number => 1, + :name => _INTL("None") +}) + +GameData::Target.register({ + :id => :User, + :id_number => 10, + :name => _INTL("User") +}) + +# Aromatic Mist, Helping Hand, Hold Hands +GameData::Target.register({ + :id => :NearAlly, + :id_number => 100, + :name => _INTL("Near Ally"), + :num_targets => 1 +}) + +# Acupressure +GameData::Target.register({ + :id => :UserOrNearAlly, + :id_number => 200, + :name => _INTL("User or Near Ally"), + :num_targets => 1 +}) + +# Aromatherapy, Gear Up, Heal Bell, Life Dew, Magnetic Flux, Howl (in Gen 8+) +GameData::Target.register({ + :id => :UserAndAllies, + :id_number => 5, + :name => _INTL("User and Allies"), + :num_targets => 2, + :long_range => true +}) + +# Me First +GameData::Target.register({ + :id => :NearFoe, + :id_number => 400, + :name => _INTL("Near Foe"), + :num_targets => 1, + :targets_foe => true +}) + +# Petal Dance, Outrage, Struggle, Thrash, Uproar +GameData::Target.register({ + :id => :RandomNearFoe, + :id_number => 2, + :name => _INTL("Random Near Foe"), + :num_targets => 1, + :targets_foe => true +}) + +GameData::Target.register({ + :id => :AllNearFoes, + :id_number => 4, + :name => _INTL("All Near Foes"), + :num_targets => 2, + :targets_foe => true +}) + +# For throwing a Poké Ball +GameData::Target.register({ + :id => :Foe, + :id_number => 9, + :name => _INTL("Foe"), + :num_targets => 1, + :targets_foe => true, + :long_range => true +}) + +# Unused +GameData::Target.register({ + :id => :AllFoes, + :id_number => 6, + :name => _INTL("All Foes"), + :num_targets => 2, + :targets_foe => true, + :long_range => true +}) + +GameData::Target.register({ + :id => :NearOther, + :id_number => 0, + :name => _INTL("Near Other"), + :num_targets => 1, + :targets_foe => true +}) + +GameData::Target.register({ + :id => :AllNearOthers, + :id_number => 8, + :name => _INTL("All Near Others"), + :num_targets => 2, + :targets_foe => true +}) + +# Most Flying-type moves, pulse moves (hits non-near targets) +GameData::Target.register({ + :id => :Other, + :id_number => 3, + :name => _INTL("Other"), + :num_targets => 1, + :targets_foe => true, + :long_range => true +}) + +# Flower Shield, Perish Song, Rototiller, Teatime +GameData::Target.register({ + :id => :AllBattlers, + :id_number => 7, + :name => _INTL("All Battlers"), + :num_targets => 2, + :targets_foe => true, + :targets_all => true, + :long_range => true +}) + +GameData::Target.register({ + :id => :UserSide, + :id_number => 40, + :name => _INTL("User Side") +}) + +# Entry hazards +GameData::Target.register({ + :id => :FoeSide, + :id_number => 80, + :name => _INTL("Foe Side"), + :affects_foe_side => true +}) + +GameData::Target.register({ + :id => :BothSides, + :id_number => 20, + :name => _INTL("Both Sides"), + :affects_foe_side => true +}) diff --git a/Data/Scripts/010_Data/002_PBS data/001_MiscPBSData.rb b/Data/Scripts/010_Data/002_PBS data/001_MiscPBSData.rb new file mode 100644 index 000000000..f60186d91 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/001_MiscPBSData.rb @@ -0,0 +1,107 @@ +#=============================================================================== +# Data caches. +#=============================================================================== +class PokemonTemp + attr_accessor :townMapData + attr_accessor :phoneData + attr_accessor :speciesShadowMovesets + attr_accessor :regionalDexes + attr_accessor :battleAnims + attr_accessor :moveToAnim + attr_accessor :mapInfos +end + +def pbClearData + if $PokemonTemp + $PokemonTemp.townMapData = nil + $PokemonTemp.phoneData = nil + $PokemonTemp.speciesShadowMovesets = nil + $PokemonTemp.regionalDexes = nil + $PokemonTemp.battleAnims = nil + $PokemonTemp.moveToAnim = nil + $PokemonTemp.mapInfos = nil + end + MapFactoryHelper.clear + $PokemonEncounters.setup($game_map.map_id) if $game_map && $PokemonEncounters + if pbRgssExists?("Data/Tilesets.rxdata") + $data_tilesets = load_data("Data/Tilesets.rxdata") + end +end + +#=============================================================================== +# Method to get Town Map data. +#=============================================================================== +def pbLoadTownMapData + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.townMapData + $PokemonTemp.townMapData = load_data("Data/town_map.dat") + end + return $PokemonTemp.townMapData +end + +#=============================================================================== +# Method to get phone call data. +#=============================================================================== +def pbLoadPhoneData + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.phoneData + if pbRgssExists?("Data/phone.dat") + $PokemonTemp.phoneData = load_data("Data/phone.dat") + end + end + return $PokemonTemp.phoneData +end + +#=============================================================================== +# Method to get Shadow Pokémon moveset data. +#=============================================================================== +def pbLoadShadowMovesets + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.speciesShadowMovesets + $PokemonTemp.speciesShadowMovesets = load_data("Data/shadow_movesets.dat") || [] + end + return $PokemonTemp.speciesShadowMovesets +end + +#=============================================================================== +# Method to get Regional Dexes data. +#=============================================================================== +def pbLoadRegionalDexes + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.regionalDexes + $PokemonTemp.regionalDexes = load_data("Data/regional_dexes.dat") + end + return $PokemonTemp.regionalDexes +end + +#=============================================================================== +# Methods relating to battle animations data. +#=============================================================================== +def pbLoadBattleAnimations + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.battleAnims + if pbRgssExists?("Data/PkmnAnimations.rxdata") + $PokemonTemp.battleAnims = load_data("Data/PkmnAnimations.rxdata") + end + end + return $PokemonTemp.battleAnims +end + +def pbLoadMoveToAnim + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.moveToAnim + $PokemonTemp.moveToAnim = load_data("Data/move2anim.dat") || [] + end + return $PokemonTemp.moveToAnim +end + +#=============================================================================== +# Method relating to map infos data. +#=============================================================================== +def pbLoadMapInfos + $PokemonTemp = PokemonTemp.new if !$PokemonTemp + if !$PokemonTemp.mapInfos + $PokemonTemp.mapInfos = load_data("Data/MapInfos.rxdata") + end + return $PokemonTemp.mapInfos +end diff --git a/Data/Scripts/010_Data/002_PBS data/002_PhoneDatabase.rb b/Data/Scripts/010_Data/002_PBS data/002_PhoneDatabase.rb new file mode 100644 index 000000000..903cf125e --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/002_PhoneDatabase.rb @@ -0,0 +1,31 @@ +#=============================================================================== +# Phone data +#=============================================================================== +class PhoneDatabase + attr_accessor :generics + attr_accessor :greetings + attr_accessor :greetingsMorning + attr_accessor :greetingsEvening + attr_accessor :bodies1 + attr_accessor :bodies2 + attr_accessor :battleRequests + attr_accessor :trainers + + def initialize + @generics = [] + @greetings = [] + @greetingsMorning = [] + @greetingsEvening = [] + @bodies1 = [] + @bodies2 = [] + @battleRequests = [] + @trainers = [] + end +end + +module PhoneMsgType + Generic = 0 + Greeting = 1 + Body = 2 + BattleRequest = 3 +end diff --git a/Data/Scripts/010_Data/002_PBS data/003_Type.rb b/Data/Scripts/010_Data/002_PBS data/003_Type.rb new file mode 100644 index 000000000..6853ffcb5 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/003_Type.rb @@ -0,0 +1,132 @@ +module GameData + class Type + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :special_type + attr_reader :pseudo_type + attr_reader :weaknesses + attr_reader :resistances + attr_reader :immunities + + DATA = {} + DATA_FILENAME = "types.dat" + + SCHEMA = { + "Name" => [1, "s"], + "InternalName" => [2, "s"], + "IsPseudoType" => [3, "b"], + "IsSpecialType" => [4, "b"], + "Weaknesses" => [5, "*s"], + "Resistances" => [6, "*s"], + "Immunities" => [7, "*s"] + } + + extend ClassMethods + include InstanceMethods + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @pseudo_type = hash[:pseudo_type] || false + @special_type = hash[:special_type] || false + @weaknesses = hash[:weaknesses] || [] + @weaknesses = [@weaknesses] if !@weaknesses.is_a?(Array) + @resistances = hash[:resistances] || [] + @resistances = [@resistances] if !@resistances.is_a?(Array) + @immunities = hash[:immunities] || [] + @immunities = [@immunities] if !@immunities.is_a?(Array) + end + + # @return [String] the translated name of this item + def name + return pbGetMessage(MessageTypes::Types, @id_number) + end + + def physical?; return !@special_type; end + def special?; return @special_type; end + + def effectiveness(other_type) + return Effectiveness::NORMAL_EFFECTIVE_ONE if !other_type + return Effectiveness::SUPER_EFFECTIVE_ONE if @weaknesses.include?(other_type) + return Effectiveness::NOT_VERY_EFFECTIVE_ONE if @resistances.include?(other_type) + return Effectiveness::INEFFECTIVE if @immunities.include?(other_type) + return Effectiveness::NORMAL_EFFECTIVE_ONE + end + end +end + +#=============================================================================== + +module Effectiveness + INEFFECTIVE = 0 + NOT_VERY_EFFECTIVE_ONE = 1 + NORMAL_EFFECTIVE_ONE = 2 + SUPER_EFFECTIVE_ONE = 4 + NORMAL_EFFECTIVE = NORMAL_EFFECTIVE_ONE ** 3 + + module_function + + def ineffective?(value) + return value == INEFFECTIVE + end + + def not_very_effective?(value) + return value > INEFFECTIVE && value < NORMAL_EFFECTIVE + end + + def resistant?(value) + return value < NORMAL_EFFECTIVE + end + + def normal?(value) + return value == NORMAL_EFFECTIVE + end + + def super_effective?(value) + return value > NORMAL_EFFECTIVE + end + + def ineffective_type?(attack_type, defend_type1, defend_type2 = nil, defend_type3 = nil) + value = calculate(attack_type, defend_type1, defend_type2, defend_type3) + return ineffective?(value) + end + + def not_very_effective_type?(attack_type, defend_type1, defend_type2 = nil, defend_type3 = nil) + value = calculate(attack_type, defend_type1, defend_type2, defend_type3) + return not_very_effective?(value) + end + + def resistant_type?(attack_type, defend_type1, defend_type2 = nil, defend_type3 = nil) + value = calculate(attack_type, defend_type1, defend_type2, defend_type3) + return resistant?(value) + end + + def normal_type?(attack_type, defend_type1, defend_type2 = nil, defend_type3 = nil) + value = calculate(attack_type, defend_type1, defend_type2, defend_type3) + return normal?(value) + end + + def super_effective_type?(attack_type, defend_type1, defend_type2 = nil, defend_type3 = nil) + value = calculate(attack_type, defend_type1, defend_type2, defend_type3) + return super_effective?(value) + end + + def calculate_one(attack_type, defend_type) + return GameData::Type.get(defend_type).effectiveness(attack_type) + end + + def calculate(attack_type, defend_type1, defend_type2 = nil, defend_type3 = nil) + mod1 = calculate_one(attack_type, defend_type1) + mod2 = NORMAL_EFFECTIVE_ONE + mod3 = NORMAL_EFFECTIVE_ONE + if defend_type2 && defend_type1 != defend_type2 + mod2 = calculate_one(attack_type, defend_type2) + end + if defend_type3 && defend_type1 != defend_type3 && defend_type2 != defend_type3 + mod3 = calculate_one(attack_type, defend_type3) + end + return mod1 * mod2 * mod3 + end +end diff --git a/Data/Scripts/010_Data/002_PBS data/004_Ability.rb b/Data/Scripts/010_Data/002_PBS data/004_Ability.rb new file mode 100644 index 000000000..2de14fccf --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/004_Ability.rb @@ -0,0 +1,31 @@ +module GameData + class Ability + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :real_description + + DATA = {} + DATA_FILENAME = "abilities.dat" + + extend ClassMethods + include InstanceMethods + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @real_description = hash[:description] || "???" + end + + # @return [String] the translated name of this ability + def name + return pbGetMessage(MessageTypes::Abilities, @id_number) + end + + # @return [String] the translated description of this ability + def description + return pbGetMessage(MessageTypes::AbilityDescs, @id_number) + end + end +end diff --git a/Data/Scripts/010_Data/002_PBS data/005_Move.rb b/Data/Scripts/010_Data/002_PBS data/005_Move.rb new file mode 100644 index 000000000..eeff3c73f --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/005_Move.rb @@ -0,0 +1,85 @@ +module GameData + class Move + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :function_code + attr_reader :base_damage + attr_reader :type + attr_reader :category + attr_reader :accuracy + attr_reader :total_pp + attr_reader :effect_chance + attr_reader :target + attr_reader :priority + attr_reader :flags + attr_reader :real_description + + DATA = {} + DATA_FILENAME = "moves.dat" + + extend ClassMethods + include InstanceMethods + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @function_code = hash[:function_code] + @base_damage = hash[:base_damage] + @type = hash[:type] + @category = hash[:category] + @accuracy = hash[:accuracy] + @total_pp = hash[:total_pp] + @effect_chance = hash[:effect_chance] + @target = hash[:target] + @priority = hash[:priority] + @flags = hash[:flags] + @real_description = hash[:description] || "???" + end + + # @return [String] the translated name of this move + def name + return pbGetMessage(MessageTypes::Moves, @id_number) + end + + # @return [String] the translated description of this move + def description + return pbGetMessage(MessageTypes::MoveDescriptions, @id_number) + end + + def physical? + return false if @base_damage == 0 + return @category == 0 if Settings::MOVE_CATEGORY_PER_MOVE + return GameData::Type.get(@type).physical? + end + + def special? + return false if @base_damage == 0 + return @category == 1 if Settings::MOVE_CATEGORY_PER_MOVE + return GameData::Type.get(@type).special? + end + + def hidden_move? + GameData::Item.each do |i| + return true if i.is_HM? && i.move == @id + end + return false + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetMoveData(move_id, move_data_type = -1) + Deprecation.warn_method('pbGetMoveData', 'v20', 'GameData::Move.get(move_id)') + return GameData::Move.get(move_id) +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsHiddenMove?(move) + Deprecation.warn_method('pbIsHiddenMove?', 'v20', 'GameData::Move.get(move).hidden_move?') + return GameData::Move.get(move).hidden_move? +end diff --git a/Data/Scripts/010_Data/002_PBS data/006_Item.rb b/Data/Scripts/010_Data/002_PBS data/006_Item.rb new file mode 100644 index 000000000..09e0257e7 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/006_Item.rb @@ -0,0 +1,324 @@ +module GameData + class Item + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :real_name_plural + attr_reader :pocket + attr_reader :price + attr_reader :real_description + attr_reader :field_use + attr_reader :battle_use + attr_reader :type + attr_reader :move + + DATA = {} + DATA_FILENAME = "items.dat" + + extend ClassMethods + include InstanceMethods + + def self.icon_filename(item) + return "Graphics/Items/back" if item.nil? + item_data = self.try_get(item) + return "Graphics/Items/000" if item_data.nil? + # Check for files + ret = sprintf("Graphics/Items/%s", item_data.id) + return ret if pbResolveBitmap(ret) + # Check for TM/HM type icons + if item_data.is_machine? + prefix = "machine" + if item_data.is_HM? + prefix = "machine_hm" + elsif item_data.is_TR? + prefix = "machine_tr" + end + move_type = GameData::Move.get(item_data.move).type + type_data = GameData::Type.get(move_type) + ret = sprintf("Graphics/Items/%s_%s", prefix, type_data.id) + return ret if pbResolveBitmap(ret) + if !item_data.is_TM? + ret = sprintf("Graphics/Items/machine_%s", type_data.id) + return ret if pbResolveBitmap(ret) + end + end + return "Graphics/Items/000" + end + + def self.list_all() + return self::DATA + end + + def self.held_icon_filename(item) + item_data = self.try_get(item) + return nil if !item_data + name_base = (item_data.is_mail?) ? "mail" : "item" + # Check for files + ret = sprintf("Graphics/Pictures/Party/icon_%s_%s", name_base, item_data.id) + return ret if pbResolveBitmap(ret) + return sprintf("Graphics/Pictures/Party/icon_%s", name_base) + end + + def self.mail_filename(item) + item_data = self.try_get(item) + return nil if !item_data + # Check for files + ret = sprintf("Graphics/Pictures/Mail/mail_%s", item_data.id) + return pbResolveBitmap(ret) ? ret : nil + end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @real_name_plural = hash[:name_plural] || "Unnamed" + @pocket = hash[:pocket] || 1 + @price = hash[:price] || 0 + @real_description = hash[:description] || "???" + @field_use = hash[:field_use] || 0 + @battle_use = hash[:battle_use] || 0 + @type = hash[:type] || 0 + @move = hash[:move] + end + + + + # @return [String] the translated name of this item + def name + return pbGetMessage(MessageTypes::Items, @id_number) + end + + # @return [String] the translated plural version of the name of this item + def name_plural + return pbGetMessage(MessageTypes::ItemPlurals, @id_number) + end + + # @return [String] the translated description of this item + def description + return pbGetMessage(MessageTypes::ItemDescriptions, @id_number) + end + + def has_battle_use? + return @battle_use != 0 + end + def is_TM?; return @field_use == 3; end + def is_HM?; return @field_use == 4; end + def is_TR?; return @field_use == 6; end + def is_machine?; return is_TM? || is_HM? || is_TR?; end + def is_mail?; return @type == 1 || @type == 2; end + def is_icon_mail?; return @type == 2; end + def is_poke_ball?; return @type == 3 || @type == 4; end + def is_snag_ball?; return @type == 3 || (@type == 4 && $Trainer.has_snag_machine); end + def is_berry?; return @type == 5; end + def is_key_item?; return @type == 6; end + def is_evolution_stone?; return @type == 7; end + def is_fossil?; return @type == 8; end + def is_apricorn?; return @type == 9; end + def is_gem?; return @type == 10; end + def is_mulch?; return @type == 11; end + def is_mega_stone?; return @type == 12; end # Does NOT include Red Orb/Blue Orb + + UNTOSSABLE_ITEMS =[:PINKANBERRY,:DYNAMITE, :TM00] + def is_important? + return true if is_key_item? || is_HM? || is_TM? + return true if UNTOSSABLE_ITEMS.include?(@id) + return false + end + + def can_hold?; return !is_important?; end + + def unlosable?(species, ability) + return true if $game_switches[SWITCH_RANDOM_HELD_ITEMS] + return false if species == :ARCEUS && ability != :MULTITYPE + return false if species == :SILVALLY && ability != :RKSSYSTEM + combos = { + :ARCEUS => [:FISTPLATE, :FIGHTINIUMZ, + :SKYPLATE, :FLYINIUMZ, + :TOXICPLATE, :POISONIUMZ, + :EARTHPLATE, :GROUNDIUMZ, + :STONEPLATE, :ROCKIUMZ, + :INSECTPLATE, :BUGINIUMZ, + :SPOOKYPLATE, :GHOSTIUMZ, + :IRONPLATE, :STEELIUMZ, + :FLAMEPLATE, :FIRIUMZ, + :SPLASHPLATE, :WATERIUMZ, + :MEADOWPLATE, :GRASSIUMZ, + :ZAPPLATE, :ELECTRIUMZ, + :MINDPLATE, :PSYCHIUMZ, + :ICICLEPLATE, :ICIUMZ, + :DRACOPLATE, :DRAGONIUMZ, + :DREADPLATE, :DARKINIUMZ, + :PIXIEPLATE, :FAIRIUMZ], + :SILVALLY => [:FIGHTINGMEMORY, + :FLYINGMEMORY, + :POISONMEMORY, + :GROUNDMEMORY, + :ROCKMEMORY, + :BUGMEMORY, + :GHOSTMEMORY, + :STEELMEMORY, + :FIREMEMORY, + :WATERMEMORY, + :GRASSMEMORY, + :ELECTRICMEMORY, + :PSYCHICMEMORY, + :ICEMEMORY, + :DRAGONMEMORY, + :DARKMEMORY, + :FAIRYMEMORY], + :GIRATINA => [:GRISEOUSORB], + :GENESECT => [:BURNDRIVE, :CHILLDRIVE, :DOUSEDRIVE, :SHOCKDRIVE], + :KYOGRE => [:BLUEORB], + :GROUDON => [:REDORB] + } + return combos[species] && combos[species].include?(@id) + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetPocket(item) + Deprecation.warn_method('pbGetPocket', 'v20', 'GameData::Item.get(item).pocket') + return GameData::Item.get(item).pocket +end + +# @deprecated This alias is slated to be removed in v20. +def pbGetPrice(item) + Deprecation.warn_method('pbGetPrice', 'v20', 'GameData::Item.get(item).price') + return GameData::Item.get(item).price +end + +# @deprecated This alias is slated to be removed in v20. +def pbGetMachine(item) + Deprecation.warn_method('pbGetMachine', 'v20', 'GameData::Item.get(item).move') + return GameData::Item.get(item).move +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsTechnicalMachine?(item) + Deprecation.warn_method('pbIsTechnicalMachine?', 'v20', 'GameData::Item.get(item).is_TM?') + return GameData::Item.get(item).is_TM? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsHiddenMachine?(item) + Deprecation.warn_method('pbIsHiddenMachine?', 'v20', 'GameData::Item.get(item).is_HM?') + return GameData::Item.get(item).is_HM? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsMachine?(item) + Deprecation.warn_method('pbIsMachine?', 'v20', 'GameData::Item.get(item).is_machine?') + return GameData::Item.get(item).is_machine? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsMail?(item) + Deprecation.warn_method('pbIsMail?', 'v20', 'GameData::Item.get(item).is_mail?') + return GameData::Item.get(item).is_mail? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsMailWithPokemonIcons?(item) + Deprecation.warn_method('pbIsMailWithPokemonIcons?', 'v20', 'GameData::Item.get(item).is_icon_mail?') + return GameData::Item.get(item).is_icon_mail? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsPokeBall?(item) + Deprecation.warn_method('pbIsPokeBall?', 'v20', 'GameData::Item.get(item).is_poke_ball?') + return GameData::Item.get(item).is_poke_ball? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsSnagBall?(item) + Deprecation.warn_method('pbIsSnagBall?', 'v20', 'GameData::Item.get(item).is_snag_ball?') + return GameData::Item.get(item).is_snag_ball? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsBerry?(item) + Deprecation.warn_method('pbIsBerry?', 'v20', 'GameData::Item.get(item).is_berry?') + return GameData::Item.get(item).is_berry? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsKeyItem?(item) + Deprecation.warn_method('pbIsKeyItem?', 'v20', 'GameData::Item.get(item).is_key_item?') + return GameData::Item.get(item).is_key_item? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsEvolutionStone?(item) + Deprecation.warn_method('pbIsEvolutionStone?', 'v20', 'GameData::Item.get(item).is_evolution_stone?') + return GameData::Item.get(item).is_evolution_stone? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsFossil?(item) + Deprecation.warn_method('pbIsFossil?', 'v20', 'GameData::Item.get(item).is_fossil?') + return GameData::Item.get(item).is_fossil? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsApricorn?(item) + Deprecation.warn_method('pbIsApricorn?', 'v20', 'GameData::Item.get(item).is_apricorn?') + return GameData::Item.get(item).is_apricorn? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsGem?(item) + Deprecation.warn_method('pbIsGem?', 'v20', 'GameData::Item.get(item).is_gem?') + return GameData::Item.get(item).is_gem? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsMulch?(item) + Deprecation.warn_method('pbIsMulch?', 'v20', 'GameData::Item.get(item).is_mulch?') + return GameData::Item.get(item).is_mulch? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsMegaStone?(item) + Deprecation.warn_method('pbIsMegaStone?', 'v20', 'GameData::Item.get(item).is_mega_stone?') + return GameData::Item.get(item).is_mega_stone? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsImportantItem?(item) + Deprecation.warn_method('pbIsImportantItem?', 'v20', 'GameData::Item.get(item).is_important?') + return GameData::Item.get(item).is_important? +end + +# @deprecated This alias is slated to be removed in v20. +def pbCanHoldItem?(item) + Deprecation.warn_method('pbCanHoldItem?', 'v20', 'GameData::Item.get(item).can_hold?') + return GameData::Item.get(item).can_hold? +end + +# @deprecated This alias is slated to be removed in v20. +def pbIsUnlosableItem?(check_item, species, ability) + Deprecation.warn_method('pbIsUnlosableItem?', 'v20', 'GameData::Item.get(item).unlosable?') + return GameData::Item.get(check_item).unlosable?(species, ability) +end + +# @deprecated This alias is slated to be removed in v20. +def pbItemIconFile(item) + Deprecation.warn_method('pbItemIconFile', 'v20', 'GameData::Item.icon_filename(item)') + return GameData::Item.icon_filename(item) +end + +# @deprecated This alias is slated to be removed in v20. +def pbHeldItemIconFile(item) + Deprecation.warn_method('pbHeldItemIconFile', 'v20', 'GameData::Item.held_icon_filename(item)') + return GameData::Item.held_icon_filename(item) +end + +# @deprecated This alias is slated to be removed in v20. +def pbMailBackFile(item) + Deprecation.warn_method('pbMailBackFile', 'v20', 'GameData::Item.mail_filename(item)') + return GameData::Item.mail_filename(item) +end diff --git a/Data/Scripts/010_Data/002_PBS data/007_BerryPlant.rb b/Data/Scripts/010_Data/002_PBS data/007_BerryPlant.rb new file mode 100644 index 000000000..ad47a4e75 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/007_BerryPlant.rb @@ -0,0 +1,36 @@ +module GameData + class BerryPlant + attr_reader :id + attr_reader :id_number + attr_reader :hours_per_stage + attr_reader :drying_per_hour + attr_reader :minimum_yield + attr_reader :maximum_yield + + DATA = {} + DATA_FILENAME = "berry_plants.dat" + + NUMBER_OF_REPLANTS = 9 + + extend ClassMethods + include InstanceMethods + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @hours_per_stage = hash[:hours_per_stage] || 3 + @drying_per_hour = hash[:drying_per_hour] || 15 + @minimum_yield = hash[:minimum_yield] || 2 + @maximum_yield = hash[:maximum_yield] || 5 + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetBerryPlantData(item) + Deprecation.warn_method('pbGetBerryPlantData', 'v20', 'GameData::BerryPlant.get(item)') + return GameData::BerryPlant.get(item) +end diff --git a/Data/Scripts/010_Data/002_PBS data/008_Species.rb b/Data/Scripts/010_Data/002_PBS data/008_Species.rb new file mode 100644 index 000000000..0136dbce2 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/008_Species.rb @@ -0,0 +1,444 @@ +module GameData + class Species + attr_reader :id + attr_reader :id_number + attr_reader :species + attr_reader :form + attr_reader :real_name + attr_reader :real_form_name + attr_reader :real_category + attr_reader :real_pokedex_entry + attr_reader :pokedex_form + attr_reader :type1 + attr_reader :type2 + attr_reader :base_stats + attr_reader :evs + attr_reader :base_exp + attr_reader :growth_rate + attr_reader :gender_ratio + attr_reader :catch_rate + attr_reader :happiness + attr_reader :moves + attr_reader :tutor_moves + attr_reader :egg_moves + attr_reader :abilities + attr_reader :hidden_abilities + attr_reader :wild_item_common + attr_reader :wild_item_uncommon + attr_reader :wild_item_rare + attr_reader :egg_groups + attr_reader :hatch_steps + attr_reader :incense + attr_reader :evolutions + attr_reader :height + attr_reader :weight + attr_reader :color + attr_reader :shape + attr_reader :habitat + attr_reader :generation + attr_reader :mega_stone + attr_reader :mega_move + attr_reader :unmega_form + attr_reader :mega_message + attr_accessor :back_sprite_x + attr_accessor :back_sprite_y + attr_accessor :front_sprite_x + attr_accessor :front_sprite_y + attr_accessor :front_sprite_altitude + attr_accessor :shadow_x + attr_accessor :shadow_size + attr_accessor :alwaysUseGeneratedSprite + + DATA = {} + DATA_FILENAME = "species.dat" + + extend ClassMethods + include InstanceMethods + + # @param species [Symbol, self, String, Integer] + # @param form [Integer] + # @return [self, nil] + def self.get_species_form(species, form) + return GameData::Species.get(species) rescue nil + # return nil if !species || !form + # validate species => [Symbol, self, String, Integer] + # validate form => Integer + # # if other.is_a?(Integer) + # # p "Please switch to symbols, thanks." + # # end + # species = species.species if species.is_a?(self) + # species = DATA[species].species if species.is_a?(Integer) + # species = species.to_sym if species.is_a?(String) + # trial = sprintf("%s_%d", species, form).to_sym + # species_form = (DATA[trial].nil?) ? species : trial + # return (DATA.has_key?(species_form)) ? DATA[species_form] : nil + end + + def self.schema(compiling_forms = false) + ret = { + "FormName" => [0, "q"], + "Kind" => [0, "s"], + "Pokedex" => [0, "q"], + "Type1" => [0, "e", :Type], + "Type2" => [0, "e", :Type], + "BaseStats" => [0, "vvvvvv"], + "EffortPoints" => [0, "uuuuuu"], + "BaseEXP" => [0, "v"], + "Rareness" => [0, "u"], + "Happiness" => [0, "u"], + "Moves" => [0, "*ue", nil, :Move], + "TutorMoves" => [0, "*e", :Move], + "EggMoves" => [0, "*e", :Move], + "Abilities" => [0, "*e", :Ability], + "HiddenAbility" => [0, "*e", :Ability], + "WildItemCommon" => [0, "e", :Item], + "WildItemUncommon" => [0, "e", :Item], + "WildItemRare" => [0, "e", :Item], + "Compatibility" => [0, "*e", :EggGroup], + "StepsToHatch" => [0, "v"], + "Height" => [0, "f"], + "Weight" => [0, "f"], + "Color" => [0, "e", :BodyColor], + "Shape" => [0, "y", :BodyShape], + "Habitat" => [0, "e", :Habitat], + "Generation" => [0, "i"], + "BattlerPlayerX" => [0, "i"], + "BattlerPlayerY" => [0, "i"], + "BattlerEnemyX" => [0, "i"], + "BattlerEnemyY" => [0, "i"], + "BattlerAltitude" => [0, "i"], + "BattlerShadowX" => [0, "i"], + "BattlerShadowSize" => [0, "u"] + } + if compiling_forms + ret["PokedexForm"] = [0, "u"] + ret["Evolutions"] = [0, "*ees", :Species, :Evolution, nil] + ret["MegaStone"] = [0, "e", :Item] + ret["MegaMove"] = [0, "e", :Move] + ret["UnmegaForm"] = [0, "u"] + ret["MegaMessage"] = [0, "u"] + else + ret["InternalName"] = [0, "n"] + ret["Name"] = [0, "s"] + ret["GrowthRate"] = [0, "e", :GrowthRate] + ret["GenderRate"] = [0, "e", :GenderRatio] + ret["Incense"] = [0, "e", :Item] + ret["Evolutions"] = [0, "*ses", nil, :Evolution, nil] + end + return ret + end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @species = hash[:species] || @id + @form = hash[:form] || 0 + @real_name = hash[:name] || "Unnamed" + @real_form_name = hash[:form_name] + @real_category = hash[:category] || "???" + @real_pokedex_entry = hash[:pokedex_entry] || "???" + @pokedex_form = hash[:pokedex_form] || @form + @type1 = hash[:type1] || :NORMAL + @type2 = hash[:type2] || @type1 + @base_stats = hash[:base_stats] || {} + @evs = hash[:evs] || {} + GameData::Stat.each_main do |s| + @base_stats[s.id] = 1 if !@base_stats[s.id] || @base_stats[s.id] <= 0 + @evs[s.id] = 0 if !@evs[s.id] || @evs[s.id] < 0 + end + @base_exp = hash[:base_exp] || 100 + @growth_rate = hash[:growth_rate] || :Medium + @gender_ratio = hash[:gender_ratio] || :Female50Percent + @catch_rate = hash[:catch_rate] || 255 + @happiness = hash[:happiness] || 70 + @moves = hash[:moves] || [] + @tutor_moves = hash[:tutor_moves] || [] + @egg_moves = hash[:egg_moves] || [] + @abilities = hash[:abilities] || [] + @hidden_abilities = hash[:hidden_abilities] || [] + @wild_item_common = hash[:wild_item_common] + @wild_item_uncommon = hash[:wild_item_uncommon] + @wild_item_rare = hash[:wild_item_rare] + @egg_groups = hash[:egg_groups] || [:Undiscovered] + @hatch_steps = hash[:hatch_steps] || 1 + @incense = hash[:incense] + @evolutions = hash[:evolutions] || [] + @height = hash[:height] || 1 + @weight = hash[:weight] || 1 + @color = hash[:color] || :Red + @shape = hash[:shape] || :Head + @habitat = hash[:habitat] || :None + @generation = hash[:generation] || 0 + @mega_stone = hash[:mega_stone] + @mega_move = hash[:mega_move] + @unmega_form = hash[:unmega_form] || 0 + @mega_message = hash[:mega_message] || 0 + @back_sprite_x = hash[:back_sprite_x] || 0 + @back_sprite_y = hash[:back_sprite_y] || 0 + @front_sprite_x = hash[:front_sprite_x] || 0 + @front_sprite_y = hash[:front_sprite_y] || 0 + @front_sprite_altitude = hash[:front_sprite_altitude] || 0 + @shadow_x = hash[:shadow_x] || 0 + @shadow_size = hash[:shadow_size] || 2 + @alwaysUseGeneratedSprite=false + end + + def set_always_use_generated_sprite(useGeneratedSprite) + @alwaysUseGeneratedSprite=useGeneratedSprite + end + + def always_use_generated + return @alwaysUseGeneratedSprite + end + # @return [String] the translated name of this species + def name + return @real_name + #return pbGetMessage(MessageTypes::Species, @id_number) + end + + # @return [String] the translated name of this form of this species + def form_name + return @real_form_name + #return pbGetMessage(MessageTypes::FormNames, @id_number) + end + + # @return [String] the translated Pokédex category of this species + def category + return @real_category + #return pbGetMessage(MessageTypes::Kinds, @id_number) + end + + # @return [String] the translated Pokédex entry of this species + def pokedex_entry + return @real_pokedex_entry + #return pbGetMessage(MessageTypes::Entries, @id_number) + end + + def is_fusion + return @id_number > Settings::NB_POKEMON + end + + def is_triple_fusion + return @id_number >= Settings::ZAPMOLCUNO_NB + end + def get_body_species + return @species + end + + def get_head_species + return @species + end + + def hasType?(type) + type = GameData::Type.get(type).id + return self.types.include?(type) + end + + def types + types = [@type1] + types << @type2 if @type2 && @type2 != @type1 + return types + end + + def apply_metrics_to_sprite(sprite, index, shadow = false) + front_sprite_y = self.is_fusion ? GameData::Species.get(getBodyID(@id_number)).front_sprite_y: @front_sprite_y + + if shadow + if (index & 1) == 1 # Foe Pokémon + sprite.x += @shadow_x * 2 + end + else + if (index & 1) == 0 # Player's Pokémon + sprite.x += @back_sprite_x * 2 + sprite.y += (@back_sprite_y * 2) + Settings::BACKSPRITE_POSITION_OFFSET + else + # Foe Pokémon + sprite.x += @front_sprite_x * 2 + sprite.y += (front_sprite_y * 2) + Settings::FRONTSPRITE_POSITION_OFFSET + sprite.y -= @front_sprite_altitude * 2 + end + end + end + + def shows_shadow? + return true + # return @front_sprite_altitude > 0 + end + + def get_evolutions(exclude_invalid = false) + ret = [] + @evolutions.each do |evo| + next if evo[3] # Is the prevolution + next if evo[1] == :None && exclude_invalid + ret.push([evo[0], evo[1], evo[2]]) # [Species, method, parameter] + end + return ret + end + + def get_family_evolutions(exclude_invalid = true) + evos = self.get_evolutions(exclude_invalid) + evos = evos.sort { |a, b| GameData::Species.get(a[0]).id_number <=> GameData::Species.get(b[0]).id_number } + ret = [] + evos.each do |evo| + ret.push([@species].concat(evo)) # [Prevo species, evo species, method, parameter] + evo_array = GameData::Species.get(evo[0]).get_family_evolutions(exclude_invalid) + ret.concat(evo_array) if evo_array && evo_array.length > 0 + end + return ret + end + + def get_previous_species + return @species if @evolutions.length == 0 + @evolutions.each { |evo| return evo[0] if evo[3] } # Is the prevolution + return @species + end + + def get_baby_species(check_items = false, item1 = nil, item2 = nil) + ret = @species + return ret if @evolutions.length == 0 + @evolutions.each do |evo| + next if !evo[3] # Not the prevolution + if check_items + incense = GameData::Species.get(evo[0]).incense + ret = evo[0] if !incense || item1 == incense || item2 == incense + else + ret = evo[0] # Species of prevolution + end + break + end + ret = GameData::Species.get(ret).get_baby_species(check_items, item1, item2) if ret != @species + return ret + end + + def get_related_species + sp = self.get_baby_species + evos = GameData::Species.get(sp).get_family_evolutions(false) + return [sp] if evos.length == 0 + return [sp].concat(evos.map { |e| e[1] }).uniq + end + + def family_evolutions_have_method?(check_method, check_param = nil) + sp = self.get_baby_species + evos = GameData::Species.get(sp).get_family_evolutions + return false if evos.length == 0 + evos.each do |evo| + if check_method.is_a?(Array) + next if !check_method.include?(evo[2]) + else + next if evo[2] != check_method + end + return true if check_param.nil? || evo[3] == check_param + end + return false + end + + # Used by the Moon Ball when checking if a Pokémon's evolution family + # includes an evolution that uses the Moon Stone. + def family_item_evolutions_use_item?(check_item = nil) + sp = self.get_baby_species + evos = GameData::Species.get(sp).get_family_evolutions + return false if !evos || evos.length == 0 + evos.each do |evo| + next if GameData::Evolution.get(evo[2]).use_item_proc.nil? + return true if check_item.nil? || evo[3] == check_item + end + return false + end + + def minimum_level + return 1 if @evolutions.length == 0 + @evolutions.each do |evo| + next if !evo[3] # Not the prevolution + evo_method_data = GameData::Evolution.get(evo[1]) + next if evo_method_data.level_up_proc.nil? + min_level = evo_method_data.minimum_level + return (min_level == 0) ? evo[2] : min_level + 1 + end + return 1 + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetSpeciesData(species, form = 0, species_data_type = -1) + Deprecation.warn_method('pbGetSpeciesData', 'v20', 'GameData::Species.get_species_form(species, form).something') + return GameData::Species.get_species_form(species, form) +end + +# @deprecated This alias is slated to be removed in v20. +def pbGetSpeciesEggMoves(species, form = 0) + Deprecation.warn_method('pbGetSpeciesEggMoves', 'v20', 'GameData::Species.get_species_form(species, form).egg_moves') + return GameData::Species.get_species_form(species, form).egg_moves +end + +# @deprecated This alias is slated to be removed in v20. +def pbGetSpeciesMoveset(species, form = 0) + Deprecation.warn_method('pbGetSpeciesMoveset', 'v20', 'GameData::Species.get_species_form(species, form).moves') + return GameData::Species.get_species_form(species, form).moves +end + +# @deprecated This alias is slated to be removed in v20. +def pbGetEvolutionData(species) + Deprecation.warn_method('pbGetEvolutionData', 'v20', 'GameData::Species.get(species).evolutions') + return GameData::Species.get(species).evolutions +end + +# @deprecated This alias is slated to be removed in v20. +def pbApplyBattlerMetricsToSprite(sprite, index, species_data, shadow = false, metrics = nil) + Deprecation.warn_method('pbApplyBattlerMetricsToSprite', 'v20', 'GameData::Species.get(species).apply_metrics_to_sprite') + GameData::Species.get(species).apply_metrics_to_sprite(sprite, index, shadow) +end + +# @deprecated This alias is slated to be removed in v20. +def showShadow?(species) + Deprecation.warn_method('showShadow?', 'v20', 'GameData::Species.get(species).shows_shadow?') + return GameData::Species.get(species).shows_shadow? +end + +# @deprecated Use {GameData#Species#get_evolutions} instead. This alias is slated to be removed in v20. +def pbGetEvolvedFormData(species, exclude_invalid = false) + Deprecation.warn_method('pbGetEvolvedFormData', 'v20', 'GameData::Species.get(species).get_evolutions') + return GameData::Species.get(species).get_evolutions(exclude_invalid) +end + +# @deprecated Use {GameData#Species#get_family_evolutions} instead. This alias is slated to be removed in v20. +def pbGetEvolutionFamilyData(species) + # Unused + Deprecation.warn_method('pbGetEvolutionFamilyData', 'v20', 'GameData::Species.get(species).get_family_evolutions') + return GameData::Species.get(species).get_family_evolutions +end + +# @deprecated Use {GameData#Species#get_previous_species} instead. This alias is slated to be removed in v20. +def pbGetPreviousForm(species) + # Unused + Deprecation.warn_method('pbGetPreviousForm', 'v20', 'GameData::Species.get(species).get_previous_species') + return GameData::Species.get(species).get_previous_species +end + +# @deprecated Use {GameData#Species#get_baby_species} instead. This alias is slated to be removed in v20. +def pbGetBabySpecies(species, check_items = false, item1 = nil, item2 = nil) + Deprecation.warn_method('pbGetBabySpecies', 'v20', 'GameData::Species.get(species).get_baby_species') + return GameData::Species.get(species).get_baby_species(check_items, item1, item2) +end + +# @deprecated Use {GameData#Species#family_evolutions_have_method?} instead. This alias is slated to be removed in v20. +def pbCheckEvolutionFamilyForMethod(species, method, param = nil) + # Unused + Deprecation.warn_method('pbCheckEvolutionFamilyForMethod', 'v20', 'GameData::Species.get(species).family_evolutions_have_method?(method)') + return GameData::Species.get(species).family_evolutions_have_method?(method, param) +end + +# @deprecated Use {GameData#Species#family_item_evolutions_use_item?} instead. This alias is slated to be removed in v20. +def pbCheckEvolutionFamilyForItemMethodItem(species, param = nil) + Deprecation.warn_method('pbCheckEvolutionFamilyForItemMethodItem', 'v20', 'GameData::Species.get(species).family_item_evolutions_use_item?(item)') + return GameData::Species.get(species).family_item_evolutions_use_item?(param) +end + +# @deprecated Use {GameData#Species#minimum_level} instead. This alias is slated to be removed in v20. +def pbGetMinimumLevel(species) + Deprecation.warn_method('pbGetMinimumLevel', 'v20', 'GameData::Species.get(species).minimum_level') + return GameData::Species.get(species).minimum_level +end diff --git a/Data/Scripts/010_Data/002_PBS data/009_Species_Files.rb b/Data/Scripts/010_Data/002_PBS data/009_Species_Files.rb new file mode 100644 index 000000000..1baf52909 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/009_Species_Files.rb @@ -0,0 +1,388 @@ +module GameData + class Species + def self.check_graphic_file(path, species, form = "", gender = 0, shiny = false, shadow = false, subfolder = "") + try_subfolder = sprintf("%s/", subfolder) + try_species = species + + try_form = form ? sprintf("_%s", form) : "" + try_gender = (gender == 1) ? "_female" : "" + try_shadow = (shadow) ? "_shadow" : "" + factors = [] + factors.push([4, sprintf("%s shiny/", subfolder), try_subfolder]) if shiny + factors.push([3, try_shadow, ""]) if shadow + factors.push([2, try_gender, ""]) if gender == 1 + factors.push([1, try_form, ""]) if form + factors.push([0, try_species, "000"]) + # Go through each combination of parameters in turn to find an existing sprite + for i in 0...2 ** factors.length + # Set try_ parameters for this combination + factors.each_with_index do |factor, index| + value = ((i / (2 ** index)) % 2 == 0) ? factor[1] : factor[2] + case factor[0] + when 0 then + try_species = value + when 1 then + try_form = value + when 2 then + try_gender = value + when 3 then + try_shadow = value + when 4 then + try_subfolder = value # Shininess + end + end + # Look for a graphic matching this combination's parameters + try_species_text = try_species + ret = pbResolveBitmap(sprintf("%s%s%s%s%s%s", path, try_subfolder, + try_species_text, try_form, try_gender, try_shadow)) + return ret if ret + end + return nil + end + + def self.check_egg_graphic_file(path, species, form, suffix = "") + species_data = self.get_species_form(species, form) + return nil if species_data.nil? + if form > 0 + ret = pbResolveBitmap(sprintf("%s%s_%d%s", path, species_data.species, form, suffix)) + return ret if ret + end + return pbResolveBitmap(sprintf("%s%s%s", path, species_data.species, suffix)) + end + + def self.front_sprite_filename(species, form = 0, gender = 0, shiny = false, shadow = false) + return self.check_graphic_file("Graphics/Pokemon/", species, form, gender, shiny, shadow, "Front") + end + + def self.back_sprite_filename(species, form = 0, gender = 0, shiny = false, shadow = false) + return self.check_graphic_file("Graphics/Pokemon/", species, form, gender, shiny, shadow, "Back") + end + + # def self.egg_sprite_filename(species, form) + # ret = self.check_egg_graphic_file("Graphics/Pokemon/Eggs/", species, form) + # return (ret) ? ret : pbResolveBitmap("Graphics/Pokemon/Eggs/000") + # end + def self.egg_sprite_filename(species, form) + return "Graphics/Battlers/Eggs/000" if $PokemonSystem.use_custom_eggs + dexNum = getDexNumberForSpecies(species) + bitmapFileName = sprintf("Graphics/Battlers/Eggs/%03d", dexNum) rescue nil + if !pbResolveBitmap(bitmapFileName) + if isTripleFusion?(dexNum) + bitmapFileName = "Graphics/Battlers/Eggs/egg_base" + else + bitmapFileName = sprintf("Graphics/Battlers/Eggs/%03d", dexNum) + if !pbResolveBitmap(bitmapFileName) + bitmapFileName = sprintf("Graphics/Battlers/Eggs/000") + end + end + end + return bitmapFileName + end + + def self.sprite_filename(species, form = 0, gender = 0, shiny = false, shadow = false, back = false, egg = false) + return self.egg_sprite_filename(species, form) if egg + return self.back_sprite_filename(species, form, gender, shiny, shadow) if back + return self.front_sprite_filename(species, form, gender, shiny, shadow) + end + + def self.front_sprite_bitmap(species, form = 0, gender = 0, shiny = false, shadow = false) + #filename = self.front_sprite_filename(species, form, gender, shiny, shadow) + filename = self.front_sprite_filename(GameData::Species.get(species).id_number) + return (filename) ? AnimatedBitmap.new(filename) : nil + end + + def self.back_sprite_bitmap(species, form = 0, gender = 0, shiny = false, shadow = false) + filename = self.back_sprite_filename(species, form, gender, shiny, shadow) + return (filename) ? AnimatedBitmap.new(filename) : nil + end + + def self.egg_sprite_bitmap(species, form = 0) + filename = self.egg_sprite_filename(species, form) + return (filename) ? AnimatedBitmap.new(filename) : nil + end + + def self.sprite_bitmap(species, form = 0, gender = 0, shiny = false, shadow = false, back = false, egg = false) + return self.egg_sprite_bitmap(species, form) if egg + return self.back_sprite_bitmap(species, form, gender, shiny, shadow) if back + return self.front_sprite_bitmap(species, shiny,) + end + + def self.sprite_bitmap_from_pokemon(pkmn, back = false, species = nil) + species = pkmn.species if !species + species = GameData::Species.get(species).species # Just to be sure it's a symbol + return self.egg_sprite_bitmap(species, pkmn.form) if pkmn.egg? + if back + ret = self.back_sprite_bitmap(species, pkmn.form, pkmn.gender, pkmn.shiny?, pkmn.shadowPokemon?) + else + ret = self.front_sprite_bitmap(species, pkmn.form, pkmn.gender, pkmn.shiny?, pkmn.shadowPokemon?) + end + alter_bitmap_function = MultipleForms.getFunction(species, "alterBitmap") + if ret && alter_bitmap_function + new_ret = ret.copy + ret.dispose + new_ret.each { |bitmap| alter_bitmap_function.call(pkmn, bitmap) } + ret = new_ret + end + print "hat" + add_hat_to_bitmap(ret,pkmn.hat,pkmn.hat_x,pkmn.hat_y) if pkmn.hat + return ret + end + + #=========================================================================== + + def self.egg_icon_filename(species, form) + ret = self.check_egg_graphic_file("Graphics/Pokemon/Eggs/", species, form, "_icon") + return (ret) ? ret : pbResolveBitmap("Graphics/Pokemon/Eggs/000_icon") + end + + def self.icon_filename(species, spriteform= nil, gender=nil, shiny = false, shadow = false, egg = false) + return self.egg_icon_filename(species, 0) if egg + return self.check_graphic_file("Graphics/Pokemon/", species, spriteform, gender, shiny, shadow, "Icons") + end + + def self.icon_filename_from_pokemon(pkmn) + return pbResolveBitmap(sprintf("Graphics/Icons/iconEgg")) if pkmn.egg? + if pkmn.isFusion? + return pbResolveBitmap(sprintf("Graphics/Icons/iconDNA")) + end + return self.icon_filename(pkmn.species, pkmn.spriteform_head, pkmn.gender, pkmn.shiny?, pkmn.shadowPokemon?, pkmn.egg?) + end + + def self.icon_filename_from_species(species) + return self.icon_filename(species, 0, 0, false, false, false) + end + + def self.egg_icon_bitmap(species, form) + filename = self.egg_icon_filename(species, form) + return (filename) ? AnimatedBitmap.new(filename).deanimate : nil + end + + def self.icon_bitmap(species, form = 0, gender = 0, shiny = false, shadow = false) + filename = self.icon_filename(species, form, gender, shiny, shadow) + return (filename) ? AnimatedBitmap.new(filename).deanimate : nil + end + + def self.icon_bitmap_from_pokemon(pkmn) + return self.icon_bitmap(pkmn.species, pkmn.form, pkmn.gender, pkmn.shiny?, pkmn.shadowPokemon?, pkmn.egg?) + end + + #=========================================================================== + + def self.footprint_filename(species, form = 0) + species_data = self.get_species_form(species, form) + return nil if species_data.nil? + if form > 0 + ret = pbResolveBitmap(sprintf("Graphics/Pokemon/Footprints/%s_%d", species_data.species, form)) + return ret if ret + end + return pbResolveBitmap(sprintf("Graphics/Pokemon/Footprints/%s", species_data.species)) + end + + #=========================================================================== + + def self.shadow_filename(species, form = 0) + species_data = self.get_species_form(species, form) + return nil if species_data.nil? + # Look for species-specific shadow graphic + if form > 0 + ret = pbResolveBitmap(sprintf("Graphics/Pokemon/Shadow/%s_%d", species_data.species, form)) + return ret if ret + end + ret = pbResolveBitmap(sprintf("Graphics/Pokemon/Shadow/%s", species_data.species)) + return ret if ret + # Use general shadow graphic + return pbResolveBitmap(sprintf("Graphics/Pokemon/Shadow/%d", species_data.shadow_size)) + end + + def self.shadow_bitmap(species, form = 0) + filename = self.shadow_filename(species, form) + return (filename) ? AnimatedBitmap.new(filename) : nil + end + + def self.shadow_bitmap_from_pokemon(pkmn) + filename = self.shadow_filename(pkmn.species, pkmn.form) + return (filename) ? AnimatedBitmap.new(filename) : nil + end + + #=========================================================================== + + def self.check_cry_file(species, form) + species_data = self.get_species_form(species, form) + return nil if species_data.nil? + if species_data.is_fusion + species_data = GameData::Species.get(getHeadID(species_data)) + end + ret = sprintf("Cries/%s", species_data.species) + return (pbResolveAudioSE(ret)) ? ret : nil + end + + def self.cry_filename(species, form = 0) + return self.check_cry_file(species, form) + end + + def self.cry_filename_from_pokemon(pkmn) + return self.check_cry_file(pkmn.species, pkmn.form) + end + + def self.play_cry_from_species(species, form = 0, volume = 90, pitch = 100) + dex_num = getDexNumberForSpecies(species) + return if !dex_num + return play_triple_fusion_cry(species, volume, pitch) if dex_num > Settings::ZAPMOLCUNO_NB + if dex_num > NB_POKEMON + body_number = getBodyID(dex_num) + head_number = getHeadID(dex_num,body_number) + return play_fusion_cry(GameData::Species.get(head_number).species,GameData::Species.get(body_number).species, volume, pitch) + end + filename = self.cry_filename(species, form) + return if !filename + pbSEPlay(RPG::AudioFile.new(filename, volume, pitch)) rescue nil + end + + def self.play_cry_from_pokemon(pkmn, volume = 90, pitch = nil) + return if !pkmn || pkmn.egg? + + species_data = pkmn.species_data + return play_triple_fusion_cry(pkmn.species, volume, pitch) if species_data.is_triple_fusion + if pkmn.species_data.is_fusion + return play_fusion_cry(species_data.get_head_species,species_data.get_body_species, volume, pitch) + end + filename = self.cry_filename_from_pokemon(pkmn) + return if !filename + pitch ||= 75 + (pkmn.hp * 25 / pkmn.totalhp) + pbSEPlay(RPG::AudioFile.new(filename, volume, pitch)) rescue nil + end + + def self.play_triple_fusion_cry(species_id, volume, pitch) + fusion_components = get_triple_fusion_components(species_id) + + echoln fusion_components + echoln species_id + for id in fusion_components + cry_filename = self.check_cry_file(id,nil) + pbSEPlay(cry_filename,volume-10) rescue nil + end + end + def self.play_fusion_cry(head_id,body_id, volume = 90, pitch = 100) + head_cry_filename = self.check_cry_file(head_id,nil) + body_cry_filename = self.check_cry_file(body_id,nil) + + pbSEPlay(body_cry_filename,volume-10) rescue nil + pbSEPlay(head_cry_filename,volume) rescue nil + end + def self.play_cry(pkmn, volume = 90, pitch = nil) + if pkmn.is_a?(Pokemon) + self.play_cry_from_pokemon(pkmn, volume, pitch) + else + self.play_cry_from_species(pkmn, nil, volume, pitch) + end + end + + def self.cry_length(species, form = 0, pitch = 100) + return 0 if !species || pitch <= 0 + pitch = pitch.to_f / 100 + ret = 0.0 + if species.is_a?(Pokemon) + if !species.egg? + filename = pbResolveAudioSE(GameData::Species.cry_filename_from_pokemon(species)) + ret = getPlayTime(filename) if filename + end + else + filename = pbResolveAudioSE(GameData::Species.cry_filename(species, form)) + ret = getPlayTime(filename) if filename + end + ret /= pitch # Sound played at a lower pitch lasts longer + return (ret * Graphics.frame_rate).ceil + 4 # 4 provides a buffer between sounds + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbLoadSpeciesBitmap(species, gender = 0, form = 0, shiny = false, shadow = false, back = false, egg = false) + Deprecation.warn_method('pbLoadSpeciesBitmap', 'v20', 'GameData::Species.sprite_bitmap(species, form, gender, shiny, shadow, back, egg)') + return GameData::Species.sprite_bitmap(species, form, gender, shiny, shadow, back, egg) +end + +# @deprecated This alias is slated to be removed in v20. +def pbLoadPokemonBitmap(pkmn, back = false) + Deprecation.warn_method('pbLoadPokemonBitmap', 'v20', 'GameData::Species.sprite_bitmap_from_pokemon(pkmn)') + return GameData::Species.sprite_bitmap_from_pokemon(pkmn, back) +end + +# @deprecated This alias is slated to be removed in v20. +def pbLoadPokemonBitmapSpecies(pkmn, species, back = false) + Deprecation.warn_method('pbLoadPokemonBitmapSpecies', 'v20', 'GameData::Species.sprite_bitmap_from_pokemon(pkmn, back, species)') + return GameData::Species.sprite_bitmap_from_pokemon(pkmn, back, species) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPokemonIconFile(pkmn) + Deprecation.warn_method('pbPokemonIconFile', 'v20', 'GameData::Species.icon_filename_from_pokemon(pkmn)') + return GameData::Species.icon_filename_from_pokemon(pkmn) +end + +# @deprecated This alias is slated to be removed in v20. +def pbLoadPokemonIcon(pkmn) + Deprecation.warn_method('pbLoadPokemonIcon', 'v20', 'GameData::Species.icon_bitmap_from_pokemon(pkmn)') + return GameData::Species.icon_bitmap_from_pokemon(pkmn) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPokemonFootprintFile(species, form = 0) + Deprecation.warn_method('pbPokemonFootprintFile', 'v20', 'GameData::Species.footprint_filename(species, form)') + return GameData::Species.footprint_filename(species, form) +end + +# @deprecated This alias is slated to be removed in v20. +def pbCheckPokemonShadowBitmapFiles(species, form = 0) + Deprecation.warn_method('pbCheckPokemonShadowBitmapFiles', 'v20', 'GameData::Species.shadow_filename(species, form)') + return GameData::Species.shadow_filename(species, form) +end + +# @deprecated This alias is slated to be removed in v20. +def pbLoadPokemonShadowBitmap(pkmn) + Deprecation.warn_method('pbLoadPokemonShadowBitmap', 'v20', 'GameData::Species.shadow_bitmap_from_pokemon(pkmn)') + return GameData::Species.shadow_bitmap_from_pokemon(pkmn) +end + +# @deprecated This alias is slated to be removed in v20. +def pbCryFile(species, form = 0) + if species.is_a?(Pokemon) + Deprecation.warn_method('pbCryFile', 'v20', 'GameData::Species.cry_filename_from_pokemon(pkmn)') + return GameData::Species.cry_filename_from_pokemon(species) + end + Deprecation.warn_method('pbCryFile', 'v20', 'GameData::Species.cry_filename(species, form)') + return GameData::Species.cry_filename(species, form) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPlayCry(pkmn, volume = 90, pitch = nil) + Deprecation.warn_method('pbPlayCry', 'v20', 'GameData::Species.play_cry(pkmn)') + GameData::Species.play_cry(pkmn, volume, pitch) +end + + +def play_cry(species, volume = 90, pitch = 100) + echoln species + GameData::Species.play_cry_from_species(species,0,volume, pitch) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPlayCrySpecies(species, form = 0, volume = 90, pitch = nil) + Deprecation.warn_method('pbPlayCrySpecies', 'v20', 'Pokemon.play_cry(species, form)') + Pokemon.play_cry(species, form, volume, pitch) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPlayCryPokemon(pkmn, volume = 90, pitch = nil) + Deprecation.warn_method('pbPlayCryPokemon', 'v20', 'pkmn.play_cry') + pkmn.play_cry(volume, pitch) +end + +# @deprecated This alias is slated to be removed in v20. +def pbCryFrameLength(species, form = 0, pitch = 100) + Deprecation.warn_method('pbCryFrameLength', 'v20', 'GameData::Species.cry_length(species, form)') + return GameData::Species.cry_length(species, form, pitch) +end diff --git a/Data/Scripts/010_Data/002_PBS data/010_Ribbon.rb b/Data/Scripts/010_Data/002_PBS data/010_Ribbon.rb new file mode 100644 index 000000000..77195cac5 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/010_Ribbon.rb @@ -0,0 +1,31 @@ +module GameData + class Ribbon + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :real_description + + DATA = {} + DATA_FILENAME = "ribbons.dat" + + extend ClassMethods + include InstanceMethods + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @real_description = hash[:description] || "???" + end + + # @return [String] the translated name of this ribbon + def name + return pbGetMessage(MessageTypes::RibbonNames, @id_number) + end + + # @return [String] the translated description of this ribbon + def description + return pbGetMessage(MessageTypes::RibbonDescriptions, @id_number) + end + end +end diff --git a/Data/Scripts/010_Data/002_PBS data/011_Encounter.rb b/Data/Scripts/010_Data/002_PBS data/011_Encounter.rb new file mode 100644 index 000000000..8085c6435 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/011_Encounter.rb @@ -0,0 +1,78 @@ +module GameData + class Encounter + attr_accessor :id + attr_accessor :map + attr_accessor :version + + attr_reader :step_chances + attr_reader :types + + DATA = {} + DATA_FILENAME = "encounters.dat" + + extend ClassMethodsSymbols + include InstanceMethods + + # @param map_id [Integer] + # @param map_version [Integer, nil] + # @return [Boolean] whether there is encounter data for the given map ID/version + def self.exists?(map_id, map_version = 0) + validate map_id => [Integer] + validate map_version => [Integer] + key = sprintf("%s_%d", map_id, map_version).to_sym + return !self::DATA[key].nil? + end + + # @param map_id [Integer] + # @param map_version [Integer, nil] + # @return [self, nil] + def self.get(map_id, map_version = 0) + validate map_id => Integer + validate map_version => Integer + trial_key = sprintf("%s_%d", map_id, map_version).to_sym + key = (self::DATA.has_key?(trial_key)) ? trial_key : sprintf("%s_0", map_id).to_sym + return self::DATA[key] + end + + # Yields all encounter data in order of their map and version numbers. + def self.each + keys = self::DATA.keys.sort do |a, b| + if self::DATA[a].map == self::DATA[b].map + self::DATA[a].version <=> self::DATA[b].version + else + self::DATA[a].map <=> self::DATA[b].map + end + end + keys.each { |key| yield self::DATA[key] } + end + + # Yields all encounter data for the given version. Also yields encounter + # data for version 0 of a map if that map doesn't have encounter data for + # the given version. + def self.each_of_version(version = 0) + self.each do |data| + yield data if data.version == version + if version > 0 + yield data if data.version == 0 && !self::DATA.has_key?([data.map, version]) + end + end + end + + def initialize(hash) + @id = hash[:id] + @map = hash[:map] + @version = hash[:version] || 0 + @step_chances = hash[:step_chances] + @types = hash[:types] || {} + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbLoadEncountersData + Deprecation.warn_method('pbLoadEncountersData', 'v20', 'GameData::Encounter.get(map_id, version)') + return nil +end diff --git a/Data/Scripts/010_Data/002_PBS data/011_EncounterModern.rb b/Data/Scripts/010_Data/002_PBS data/011_EncounterModern.rb new file mode 100644 index 000000000..a1791bcad --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/011_EncounterModern.rb @@ -0,0 +1,77 @@ +module GameData + class EncounterModern + attr_accessor :id + attr_accessor :map + attr_accessor :version + attr_reader :step_chances + attr_reader :types + + DATA = {} + DATA_FILENAME = "encounters_remix.dat" + + extend ClassMethodsSymbols + include InstanceMethods + + # @param map_id [Integer] + # @param map_version [Integer, nil] + # @return [Boolean] whether there is encounter data for the given map ID/version + def self.exists?(map_id, map_version = 0) + validate map_id => [Integer] + validate map_version => [Integer] + key = sprintf("%s_%d", map_id, map_version).to_sym + return !self::DATA[key].nil? + end + + # @param map_id [Integer] + # @param map_version [Integer, nil] + # @return [self, nil] + def self.get(map_id, map_version = 0) + validate map_id => Integer + validate map_version => Integer + trial_key = sprintf("%s_%d", map_id, map_version).to_sym + key = (self::DATA.has_key?(trial_key)) ? trial_key : sprintf("%s_0", map_id).to_sym + return self::DATA[key] + end + + # Yields all encounter data in order of their map and version numbers. + def self.each + keys = self::DATA.keys.sort do |a, b| + if self::DATA[a].map == self::DATA[b].map + self::DATA[a].version <=> self::DATA[b].version + else + self::DATA[a].map <=> self::DATA[b].map + end + end + keys.each { |key| yield self::DATA[key] } + end + + # Yields all encounter data for the given version. Also yields encounter + # data for version 0 of a map if that map doesn't have encounter data for + # the given version. + def self.each_of_version(version = 0) + self.each do |data| + yield data if data.version == version + if version > 0 + yield data if data.version == 0 && !self::DATA.has_key?([data.map, version]) + end + end + end + + def initialize(hash) + @id = hash[:id] + @map = hash[:map] + @version = hash[:version] || 0 + @step_chances = hash[:step_chances] + @types = hash[:types] || {} + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbLoadEncountersData + Deprecation.warn_method('pbLoadEncountersData', 'v20', 'GameData::Encounter.get(map_id, version)') + return nil +end diff --git a/Data/Scripts/010_Data/002_PBS data/011_Encounter_random.rb b/Data/Scripts/010_Data/002_PBS data/011_Encounter_random.rb new file mode 100644 index 000000000..a87311705 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/011_Encounter_random.rb @@ -0,0 +1,77 @@ +module GameData + class EncounterRandom + attr_accessor :id + attr_accessor :map + attr_accessor :version + attr_reader :step_chances + attr_reader :types + + DATA = {} + DATA_FILENAME = "encounters_randomized.dat" + + extend ClassMethodsSymbols + include InstanceMethods + + # @param map_id [Integer] + # @param map_version [Integer, nil] + # @return [Boolean] whether there is encounter data for the given map ID/version + def self.exists?(map_id, map_version = 0) + validate map_id => [Integer] + validate map_version => [Integer] + key = sprintf("%s_%d", map_id, map_version).to_sym + return !self::DATA[key].nil? + end + + # @param map_id [Integer] + # @param map_version [Integer, nil] + # @return [self, nil] + def self.get(map_id, map_version = 0) + validate map_id => Integer + validate map_version => Integer + trial_key = sprintf("%s_%d", map_id, map_version).to_sym + key = (self::DATA.has_key?(trial_key)) ? trial_key : sprintf("%s_0", map_id).to_sym + return self::DATA[key] + end + + # Yields all encounter data in order of their map and version numbers. + def self.each + keys = self::DATA.keys.sort do |a, b| + if self::DATA[a].map == self::DATA[b].map + self::DATA[a].version <=> self::DATA[b].version + else + self::DATA[a].map <=> self::DATA[b].map + end + end + keys.each { |key| yield self::DATA[key] } + end + + # Yields all encounter data for the given version. Also yields encounter + # data for version 0 of a map if that map doesn't have encounter data for + # the given version. + def self.each_of_version(version = 0) + self.each do |data| + yield data if data.version == version + if version > 0 + yield data if data.version == 0 && !self::DATA.has_key?([data.map, version]) + end + end + end + + def initialize(hash) + @id = hash[:id] + @map = hash[:map] + @version = hash[:version] || 0 + @step_chances = hash[:step_chances] + @types = hash[:types] || {} + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbLoadEncountersData + Deprecation.warn_method('pbLoadEncountersData', 'v20', 'GameData::Encounter.get(map_id, version)') + return nil +end diff --git a/Data/Scripts/010_Data/002_PBS data/012_TrainerType.rb b/Data/Scripts/010_Data/002_PBS data/012_TrainerType.rb new file mode 100644 index 000000000..e583703dd --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/012_TrainerType.rb @@ -0,0 +1,157 @@ +module GameData + class TrainerType + attr_reader :id + attr_reader :id_number + attr_reader :real_name + attr_reader :base_money + attr_reader :battle_BGM + attr_reader :victory_ME + attr_reader :intro_ME + attr_reader :gender + attr_reader :skill_level + attr_reader :skill_code + + DATA = {} + DATA_FILENAME = "trainer_types.dat" + + extend ClassMethods + include InstanceMethods + + def self.check_file(tr_type, path, optional_suffix = "", suffix = "") + tr_type_data = self.try_get(tr_type) + return nil if tr_type_data.nil? + # Check for files + if optional_suffix && !optional_suffix.empty? + ret = path + tr_type_data.id.to_s + optional_suffix + suffix + return ret if pbResolveBitmap(ret) + ret = path + sprintf("%03d", tr_type_data.id_number) + optional_suffix + suffix + return ret if pbResolveBitmap(ret) + end + ret = path + tr_type_data.id.to_s + suffix + return ret if pbResolveBitmap(ret) + ret = path + sprintf("%03d", tr_type_data.id_number) + suffix + return (pbResolveBitmap(ret)) ? ret : nil + end + + def self.charset_filename(tr_type) + return self.check_file(tr_type, "Graphics/Characters/trainer_") + end + + def self.charset_filename_brief(tr_type) + ret = self.charset_filename(tr_type) + ret.slice!("Graphics/Characters/") if ret + return ret + end + + def self.front_sprite_filename(tr_type) + tr_type_data = self.try_get(tr_type) + path = "Graphics/Trainers/" + file = sprintf("trainer%03d", tr_type_data.id_number) + ret = path + file + return ret if pbResolveBitmap(ret) + end + + def self.player_front_sprite_filename(tr_type) + #outfit = ($Trainer) ? $Trainer.outfit : 0 + outfit=0 + return self.check_file(tr_type, "Graphics/Trainers/", sprintf("_%d", outfit)) + end + + def self.back_sprite_filename(tr_type) + return self.check_file(tr_type, "Graphics/Trainers/", "", "_back") + end + + def self.player_back_sprite_filename(tr_type) + #outfit = ($Trainer) ? $Trainer.outfit : 0 + outfit=0 + return self.check_file(tr_type, "Graphics/Trainers/", sprintf("_%d", outfit), "_back") + end + + def self.map_icon_filename(tr_type) + return self.check_file(tr_type, "Graphics/Pictures/mapPlayer") + end + + def self.player_map_icon_filename(tr_type) + outfit = ($Trainer) ? $Trainer.outfit : 0 + return self.check_file(tr_type, "Graphics/Pictures/mapPlayer", sprintf("_%d", outfit)) + end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] || -1 + @real_name = hash[:name] || "Unnamed" + @base_money = hash[:base_money] || 30 + @battle_BGM = hash[:battle_BGM] + @victory_ME = hash[:victory_ME] + @intro_ME = hash[:intro_ME] + @gender = hash[:gender] || 2 + @skill_level = hash[:skill_level] || @base_money + @skill_code = hash[:skill_code] + end + + # @return [String] the translated name of this trainer type + def name + return pbGetMessage(MessageTypes::TrainerTypes, @id_number) + end + + def male?; return @gender == 0; end + def female?; return @gender == 1; end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetTrainerTypeData(tr_type) + Deprecation.warn_method('pbGetTrainerTypeData', 'v20', 'GameData::TrainerType.get(trainer_type)') + return GameData::TrainerType.get(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbTrainerCharFile(tr_type) # Used by the phone + Deprecation.warn_method('pbTrainerCharFile', 'v20', 'GameData::TrainerType.charset_filename(trainer_type)') + return GameData::TrainerType.charset_filename(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbTrainerCharNameFile(tr_type) # Used by Battle Frontier and compiler + Deprecation.warn_method('pbTrainerCharNameFile', 'v20', 'GameData::TrainerType.charset_filename_brief(trainer_type)') + return GameData::TrainerType.charset_filename_brief(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbTrainerSpriteFile(tr_type) + Deprecation.warn_method('pbTrainerSpriteFile', 'v20', 'GameData::TrainerType.front_sprite_filename(trainer_type)') + return GameData::TrainerType.front_sprite_filename(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbTrainerSpriteBackFile(tr_type) + Deprecation.warn_method('pbTrainerSpriteBackFile', 'v20', 'GameData::TrainerType.back_sprite_filename(trainer_type)') + return GameData::TrainerType.back_sprite_filename(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPlayerSpriteFile(tr_type) + Deprecation.warn_method('pbPlayerSpriteFile', 'v20', 'GameData::TrainerType.player_front_sprite_filename(trainer_type)') + return GameData::TrainerType.player_front_sprite_filename(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPlayerSpriteBackFile(tr_type) + Deprecation.warn_method('pbPlayerSpriteBackFile', 'v20', 'GameData::TrainerType.player_back_sprite_filename(trainer_type)') + return GameData::TrainerType.player_back_sprite_filename(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbTrainerHeadFile(tr_type) + Deprecation.warn_method('pbTrainerHeadFile', 'v20', 'GameData::TrainerType.map_icon_filename(trainer_type)') + return GameData::TrainerType.map_icon_filename(tr_type) +end + +# @deprecated This alias is slated to be removed in v20. +def pbPlayerHeadFile(tr_type) + Deprecation.warn_method('pbPlayerHeadFile', 'v20', 'GameData::TrainerType.player_map_icon_filename(trainer_type)') + return GameData::TrainerType.player_map_icon_filename(tr_type) +end diff --git a/Data/Scripts/010_Data/002_PBS data/013_Trainer.rb b/Data/Scripts/010_Data/002_PBS data/013_Trainer.rb new file mode 100644 index 000000000..e6bbb1bce --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/013_Trainer.rb @@ -0,0 +1,442 @@ +module GameData + class Trainer + attr_reader :id + attr_reader :id_number + attr_reader :trainer_type + attr_reader :real_name + attr_reader :version + attr_reader :items + attr_reader :real_lose_text + attr_reader :pokemon + + + attr_accessor :loseText_rematch + attr_accessor :preRematchText + attr_accessor :preRematchText_caught + attr_accessor :preRematchText_evolved + attr_accessor :preRematchText_fused + attr_accessor :preRematchText_unfused + attr_accessor :preRematchText_reversed + + DATA = {} + DATA_FILENAME = "trainers.dat" + + SCHEMA = { + "Items" => [:items, "*e", :Item], + "LoseText" => [:lose_text, "s"], + "Pokemon" => [:pokemon, "ev", :Species], # Species, level + "Form" => [:form, "u"], + "Name" => [:name, "s"], + "Moves" => [:moves, "*e", :Move], + "Ability" => [:ability, "s"], + "AbilityIndex" => [:ability_index, "u"], + "Item" => [:item, "e", :Item], + "Gender" => [:gender, "e", { "M" => 0, "m" => 0, "Male" => 0, "male" => 0, "0" => 0, + "F" => 1, "f" => 1, "Female" => 1, "female" => 1, "1" => 1 }], + "Nature" => [:nature, "e", :Nature], + "IV" => [:iv, "uUUUUU"], + "EV" => [:ev, "uUUUUU"], + "Happiness" => [:happiness, "u"], + "Shiny" => [:shininess, "b"], + "Shadow" => [:shadowness, "b"], + "Ball" => [:poke_ball, "s"], + + "LoseTextRematch" => [:loseText_rematch, "s"], + "PreRematchText" => [:preRematchText, "s"], + "PreRematchText_caught" => [:preRematchText_caught, "s"], + "PreRematchText_evolved" => [:preRematchText_evolved, "s"], + "PreRematchText_fused" => [:preRematchText_fused, "s"], + "PreRematchText_unfused" => [:preRematchText_unfused, "s"], + "PreRematchText_reversed" => [:preRematchText_reversed, "s"], + + + } + + extend ClassMethods + include InstanceMethods + + # @param tr_type [Symbol, String] + # @param tr_name [String] + # @param tr_version [Integer, nil] + # @return [Boolean] whether the given other is defined as a self + def self.exists?(tr_type, tr_name, tr_version = 0) + validate tr_type => [Symbol, String] + validate tr_name => [String] + key = [tr_type.to_sym, tr_name, tr_version] + return !self::DATA[key].nil? + end + + # @param tr_type [Symbol, String] + # @param tr_name [String] + # @param tr_version [Integer, nil] + # @return [self] + def self.get(tr_type, tr_name, tr_version = 0) + validate tr_type => [Symbol, String] + validate tr_name => [String] + key = [tr_type.to_sym, tr_name, tr_version] + raise "Unknown trainer #{tr_type} #{tr_name} #{tr_version}." unless self::DATA.has_key?(key) + return self::DATA[key] + end + + def list_all + return self::DATA + end + + # @param tr_type [Symbol, String] + # @param tr_name [String] + # @param tr_version [Integer, nil] + # @return [self, nil] + def self.try_get(tr_type, tr_name, tr_version = 0) + validate tr_type => [Symbol, String] + validate tr_name => [String] + key = [tr_type.to_sym, tr_name, tr_version] + return (self::DATA.has_key?(key)) ? self::DATA[key] : nil + end + + def self.list_all() + return self::DATA + end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] + @trainer_type = hash[:trainer_type] + @real_name = hash[:name] || "Unnamed" + @version = hash[:version] || 0 + @items = hash[:items] || [] + @real_lose_text = hash[:lose_text] || "..." + @pokemon = hash[:pokemon] || [] + @pokemon.each do |pkmn| + GameData::Stat.each_main do |s| + pkmn[:iv][s.id] ||= 0 if pkmn[:iv] + pkmn[:ev][s.id] ||= 0 if pkmn[:ev] + end + end + + @loseText_rematch = hash[:loseText_rematch] || @real_lose_text + @preRematchText = hash[:preRematchText] || "Are you up for a rematch? Or maybe you want to trade..." + @preRematchText_caught = hash[:preRematchText_caught] || @preRematchText + @preRematchText_evolved = hash[:preRematchText_evolved] || @preRematchText + @preRematchText_fused = hash[:preRematchText_fused] || @preRematchText + @preRematchText_unfused = hash[:preRematchText_unfused] || @preRematchText + @preRematchText_reversed = hash[:preRematchText_reversed] || @preRematchText + end + + # @return [String] the translated name of this trainer + def name + return pbGetMessageFromHash(MessageTypes::TrainerNames, @real_name) + end + + # @return [String] the translated in-battle lose message of this trainer + def lose_text + return pbGetMessageFromHash(MessageTypes::TrainerLoseText, @real_lose_text) + end + + def rematch_lose_text + return pbGetMessageFromHash(MessageTypes::TrainerLoseText, @loseText_rematch) + end + + def preRematch_text_default + return pbGetMessageFromHash(MessageTypes::BeginSpeech, @preRematchText) + end + + def preRematch_text_caught + return pbGetMessageFromHash(MessageTypes::BeginSpeech, @preRematchText_caught) + end + + def preRematch_text_evolved + return pbGetMessageFromHash(MessageTypes::BeginSpeech, @preRematchText_evolved) + end + + def preRematch_text_fused + return pbGetMessageFromHash(MessageTypes::BeginSpeech, @preRematchText_fused) + end + + def preRematch_text_unfused + return pbGetMessageFromHash(MessageTypes::BeginSpeech, @preRematchText_unfused) + end + + def preRematch_text_reversed + return pbGetMessageFromHash(MessageTypes::BeginSpeech, @preRematchText_reversed) + end + + def replace_species_with_placeholder(species) + case species + when Settings::RIVAL_STARTER_PLACEHOLDER_SPECIES + return pbGet(Settings::RIVAL_STARTER_PLACEHOLDER_VARIABLE) + when Settings::VAR_1_PLACEHOLDER_SPECIES + return pbGet(1) + when Settings::VAR_2_PLACEHOLDER_SPECIES + return pbGet(2) + when Settings::VAR_3_PLACEHOLDER_SPECIES + return pbGet(3) + end + end + + def generateRandomChampionSpecies(old_species) + customsList = getCustomSpeciesList() + bst_range = pbGet(VAR_RANDOMIZER_TRAINER_BST) + new_species = $game_switches[SWITCH_RANDOM_GYM_CUSTOMS] ? getSpecies(getNewCustomSpecies(old_species, customsList, bst_range)) : getSpecies(getNewSpecies(old_species, bst_range)) + #every pokemon should be fully evolved + evolved_species_id = getEvolution(new_species) + evolved_species_id = getEvolution(evolved_species_id) + evolved_species_id = getEvolution(evolved_species_id) + evolved_species_id = getEvolution(evolved_species_id) + return getSpecies(evolved_species_id) + end + + def generateRandomGymSpecies(old_species) + gym_index = pbGet(VAR_CURRENT_GYM_TYPE) + return old_species if gym_index == -1 + return generateRandomChampionSpecies(old_species) if gym_index == 999 + type_id = pbGet(VAR_GYM_TYPES_ARRAY)[gym_index] + return old_species if type_id == -1 + + customsList = getCustomSpeciesList() + bst_range = pbGet(VAR_RANDOMIZER_TRAINER_BST) + gym_type = GameData::Type.get(type_id) + while true + new_species = $game_switches[SWITCH_RANDOM_GYM_CUSTOMS] ? getSpecies(getNewCustomSpecies(old_species, customsList, bst_range)) : getSpecies(getNewSpecies(old_species, bst_range)) + if new_species.hasType?(gym_type) + return new_species + end + end + end + + def replace_species_to_randomized_gym(species, trainerId, pokemonIndex) + return if !pokemonIndex + return if !trainerId + return if !species + if $PokemonGlobal.randomGymTrainersHash == nil + $PokemonGlobal.randomGymTrainersHash = {} + end + if $game_switches[SWITCH_RANDOM_GYM_PERSIST_TEAMS] && $PokemonGlobal.randomGymTrainersHash != nil + if $PokemonGlobal.randomGymTrainersHash[trainerId] && $PokemonGlobal.randomGymTrainersHash[trainerId].length >= $PokemonGlobal.randomTrainersHash[trainerId].length + newSpecies = getSpecies($PokemonGlobal.randomGymTrainersHash[trainerId][pokemonIndex]) + return newSpecies if newSpecies + return species + end + end + new_species = generateRandomGymSpecies(species) + if $game_switches[SWITCH_RANDOM_GYM_PERSIST_TEAMS] + add_generated_species_to_gym_array(new_species, trainerId) + end + return new_species + end + + def add_generated_species_to_gym_array(new_species, trainerId) + if (new_species.is_a?(Symbol)) + id = new_species + else + id = new_species.id_number + end + + expected_team_length = 1 + expected_team_length = $PokemonGlobal.randomTrainersHash[trainerId].length if $PokemonGlobal.randomTrainersHash[trainerId] + new_team = [] + if $PokemonGlobal.randomGymTrainersHash[trainerId] + new_team = $PokemonGlobal.randomGymTrainersHash[trainerId] + end + if new_team.length < expected_team_length + new_team << id + end + $PokemonGlobal.randomGymTrainersHash[trainerId] = new_team + end + + def replace_species_to_randomized_regular(species, trainerId, pokemonIndex) + if $PokemonGlobal.randomTrainersHash[trainerId] == nil + Kernel.pbMessage(_INTL("The trainers need to be re-shuffled.")) + Kernel.pbShuffleTrainers() + end + new_species_dex = $PokemonGlobal.randomTrainersHash[trainerId][pokemonIndex] + return getSpecies(new_species_dex) + end + + def isGymBattle + return ($game_switches[SWITCH_RANDOM_TRAINERS] && ($game_variables[VAR_CURRENT_GYM_TYPE] != -1) || ($game_switches[SWITCH_FIRST_RIVAL_BATTLE] && $game_switches[SWITCH_RANDOM_STARTERS])) + end + + def replace_species_to_randomized(species, trainerId, pokemonIndex) + return species if $game_switches[SWITCH_DONT_RANDOMIZE] + return species if $game_switches[SWITCH_FIRST_RIVAL_BATTLE] + return species if getDexNumberForSpecies(species) >= Settings::ZAPMOLCUNO_NB + if isGymBattle() && $game_switches[SWITCH_RANDOMIZE_GYMS_SEPARATELY] + return replace_species_to_randomized_gym(species, trainerId, pokemonIndex) + end + return replace_species_to_randomized_regular(species, trainerId, pokemonIndex) + + end + + def replaceSingleSpeciesModeIfApplicable(species) + return species if getDexNumberForSpecies(species) >= Settings::ZAPMOLCUNO_NB + if $game_switches[SWITCH_SINGLE_POKEMON_MODE] + if $game_switches[SWITCH_SINGLE_POKEMON_MODE_HEAD] + return replaceFusionsHeadWithSpecies(species) + elsif $game_switches[SWITCH_SINGLE_POKEMON_MODE_BODY] + return replaceFusionsBodyWithSpecies(species) + elsif $game_switches[SWITCH_SINGLE_POKEMON_MODE_RANDOM] + if (rand(2) == 0) + return replaceFusionsHeadWithSpecies(species) + else + return replaceFusionsBodyWithSpecies(species) + end + end + end + return species + end + + def replaceFusionsHeadWithSpecies(species) + speciesId = getDexNumberForSpecies(species) + if speciesId > NB_POKEMON + bodyPoke = getBodyID(speciesId) + headPoke = pbGet(VAR_SINGLE_POKEMON_MODE) + newSpecies = bodyPoke * NB_POKEMON + headPoke + return getPokemon(newSpecies) + end + return species + end + + def replaceFusionsBodyWithSpecies(species) + speciesId = getDexNumberForSpecies(species) + if speciesId > NB_POKEMON + bodyPoke = pbGet(VAR_SINGLE_POKEMON_MODE) + headPoke = getHeadID(species) + newSpecies = bodyPoke * NB_POKEMON + headPoke + return getPokemon(newSpecies) + end + return species + end + + def to_trainer + placeholder_species = [Settings::RIVAL_STARTER_PLACEHOLDER_SPECIES, + Settings::VAR_1_PLACEHOLDER_SPECIES, + Settings::VAR_2_PLACEHOLDER_SPECIES, + Settings::VAR_3_PLACEHOLDER_SPECIES] + # Determine trainer's name + tr_name = self.name + Settings::RIVAL_NAMES.each do |rival| + next if rival[0] != @trainer_type || !$game_variables[rival[1]].is_a?(String) + tr_name = $game_variables[rival[1]] + break + end + # Create trainer object + trainer = NPCTrainer.new(tr_name, @trainer_type) + trainer.id = $Trainer.make_foreign_ID + trainer.items = @items.clone + trainer.lose_text = self.lose_text + + isRematch = $game_switches[SWITCH_IS_REMATCH] + isPlayingRandomized = $game_switches[SWITCH_RANDOM_TRAINERS] && !$game_switches[SWITCH_FIRST_RIVAL_BATTLE] + rematchId = getRematchId(trainer.name, trainer.trainer_type) + + # Create each Pokémon owned by the trainer + index = 0 + @pokemon.each do |pkmn_data| + #replace placeholder species infinite fusion edit + species = GameData::Species.get(pkmn_data[:species]).species + original_species = species + if placeholder_species.include?(species) + species = replace_species_with_placeholder(species) + else + species = replace_species_to_randomized(species, self.id, index) if isPlayingRandomized + end + species = replaceSingleSpeciesModeIfApplicable(species) + if $game_switches[SWITCH_REVERSED_MODE] + species = reverseFusionSpecies(species) + end + level = pkmn_data[:level] + if $game_switches[SWITCH_GAME_DIFFICULTY_HARD] + level = (level * Settings::HARD_MODE_LEVEL_MODIFIER).ceil + if level > Settings::MAXIMUM_LEVEL + level = Settings::MAXIMUM_LEVEL + end + end + + if $game_switches[Settings::OVERRIDE_BATTLE_LEVEL_SWITCH] + override_level = $game_variables[Settings::OVERRIDE_BATTLE_LEVEL_VALUE_VAR] + if override_level.is_a?(Integer) + level = override_level + end + end + #### + + #trainer rematch infinite fusion edit + if isRematch + nbRematch = getNumberRematch(rematchId) + level = getRematchLevel(level, nbRematch) + species = getSpecies(evolveRematchPokemon(nbRematch, species)).id + end + pkmn = Pokemon.new(species, level, trainer, false) + + trainer.party.push(pkmn) + # Set Pokémon's properties if defined + if pkmn_data[:form] + pkmn.forced_form = pkmn_data[:form] if MultipleForms.hasFunction?(species, "getForm") + pkmn.form_simple = pkmn_data[:form] + end + + if $game_switches[SWITCH_RANDOM_HELD_ITEMS] + pkmn.item = pbGetRandomHeldItem().id + else + pkmn.item = pkmn_data[:item] + end + if pkmn_data[:moves] && pkmn_data[:moves].length > 0 && original_species == species + pkmn_data[:moves].each { |move| pkmn.learn_move(move) } + else + pkmn.reset_moves + end + pkmn.ability_index = pkmn_data[:ability_index] + pkmn.ability = pkmn_data[:ability] + + if $game_switches[SWITCH_DOUBLE_ABILITIES] && pkmn.isFusion? + secondary_ability_index = pkmn.ability_index == 0 ? 1 : 0 + pkmn.ability2_index = secondary_ability_index + pkmn.ability2 = pkmn.getAbilityList[secondary_ability_index][0] + #print _INTL("Primary: {1}, Secondary: {2}",pkmn.ability.id, pkmn.ability2.id) + end + + pkmn.gender = pkmn_data[:gender] || ((trainer.male?) ? 0 : 1) + pkmn.shiny = (pkmn_data[:shininess]) ? true : false + if pkmn_data[:nature] + pkmn.nature = pkmn_data[:nature] + else + nature = pkmn.species_data.id_number + GameData::TrainerType.get(trainer.trainer_type).id_number + pkmn.nature = nature % (GameData::Nature::DATA.length / 2) + end + GameData::Stat.each_main do |s| + if pkmn_data[:iv] + pkmn.iv[s.id] = pkmn_data[:iv][s.id] + else + pkmn.iv[s.id] = [pkmn_data[:level] / 2, Pokemon::IV_STAT_LIMIT].min + end + if pkmn_data[:ev] + pkmn.ev[s.id] = pkmn_data[:ev][s.id] + else + pkmn.ev[s.id] = [pkmn_data[:level] * 3 / 2, Pokemon::EV_LIMIT / 6].min + end + end + pkmn.happiness = pkmn_data[:happiness] if pkmn_data[:happiness] + pkmn.name = pkmn_data[:name] if pkmn_data[:name] && !pkmn_data[:name].empty? + if pkmn_data[:shadowness] + pkmn.makeShadow + pkmn.update_shadow_moves(true) + pkmn.shiny = false + end + pkmn.poke_ball = pkmn_data[:poke_ball] if pkmn_data[:poke_ball] + pkmn.calc_stats + + index += 1 + end + return trainer + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetTrainerData(tr_type, tr_name, tr_version = 0) + Deprecation.warn_method('pbGetTrainerData', 'v20', 'GameData::Trainer.get(tr_type, tr_name, tr_version)') + return GameData::Trainer.get(tr_type, tr_name, tr_version) +end diff --git a/Data/Scripts/010_Data/002_PBS data/013_TrainerExpert.rb b/Data/Scripts/010_Data/002_PBS data/013_TrainerExpert.rb new file mode 100644 index 000000000..bb33b3472 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/013_TrainerExpert.rb @@ -0,0 +1,14 @@ +module GameData + class TrainerExpert < Trainer + DATA_FILENAME = "trainers_expert.dat" + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetTrainerData(tr_type, tr_name, tr_version = 0) + Deprecation.warn_method('pbGetTrainerData', 'v20', 'GameData::Trainer.get(tr_type, tr_name, tr_version)') + return GameData::Trainer.get(tr_type, tr_name, tr_version) +end diff --git a/Data/Scripts/010_Data/002_PBS data/013_TrainerModern.rb b/Data/Scripts/010_Data/002_PBS data/013_TrainerModern.rb new file mode 100644 index 000000000..1b713fae4 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/013_TrainerModern.rb @@ -0,0 +1,369 @@ +module GameData + class TrainerModern + attr_reader :id + attr_reader :id_number + attr_reader :trainer_type + attr_reader :real_name + attr_reader :version + attr_reader :items + attr_reader :real_lose_text + attr_reader :pokemon + + DATA = {} + DATA_FILENAME = "trainers_remix.dat" + + SCHEMA = { + "Items" => [:items, "*e", :Item], + "LoseText" => [:lose_text, "s"], + "Pokemon" => [:pokemon, "ev", :Species], # Species, level + "Form" => [:form, "u"], + "Name" => [:name, "s"], + "Moves" => [:moves, "*e", :Move], + "Ability" => [:ability, "s"], + "AbilityIndex" => [:ability_index, "u"], + "Item" => [:item, "e", :Item], + "Gender" => [:gender, "e", { "M" => 0, "m" => 0, "Male" => 0, "male" => 0, "0" => 0, + "F" => 1, "f" => 1, "Female" => 1, "female" => 1, "1" => 1 }], + "Nature" => [:nature, "e", :Nature], + "IV" => [:iv, "uUUUUU"], + "EV" => [:ev, "uUUUUU"], + "Happiness" => [:happiness, "u"], + "Shiny" => [:shininess, "b"], + "Shadow" => [:shadowness, "b"], + "Ball" => [:poke_ball, "s"], + } + + extend ClassMethods + include InstanceMethods + + # @param tr_type [Symbol, String] + # @param tr_name [String] + # @param tr_version [Integer, nil] + # @return [Boolean] whether the given other is defined as a self + def self.exists?(tr_type, tr_name, tr_version = 0) + validate tr_type => [Symbol, String] + validate tr_name => [String] + key = [tr_type.to_sym, tr_name, tr_version] + return !self::DATA[key].nil? + end + + # @param tr_type [Symbol, String] + # @param tr_name [String] + # @param tr_version [Integer, nil] + # @return [self] + def self.get(tr_type, tr_name, tr_version = 0) + validate tr_type => [Symbol, String] + validate tr_name => [String] + key = [tr_type.to_sym, tr_name, tr_version] + raise "Unknown trainer #{tr_type} #{tr_name} #{tr_version}." unless self::DATA.has_key?(key) + return self::DATA[key] + end + + # @param tr_type [Symbol, String] + # @param tr_name [String] + # @param tr_version [Integer, nil] + # @return [self, nil] + def self.try_get(tr_type, tr_name, tr_version = 0) + validate tr_type => [Symbol, String] + validate tr_name => [String] + key = [tr_type.to_sym, tr_name, tr_version] + return (self::DATA.has_key?(key)) ? self::DATA[key] : nil + end + + def self.list_all() + return self::DATA + end + + def initialize(hash) + @id = hash[:id] + @id_number = hash[:id_number] + @trainer_type = hash[:trainer_type] + @real_name = hash[:name] || "Unnamed" + @version = hash[:version] || 0 + @items = hash[:items] || [] + @real_lose_text = hash[:lose_text] || "..." + @pokemon = hash[:pokemon] || [] + @pokemon.each do |pkmn| + GameData::Stat.each_main do |s| + pkmn[:iv][s.id] ||= 0 if pkmn[:iv] + pkmn[:ev][s.id] ||= 0 if pkmn[:ev] + end + end + end + + # @return [String] the translated name of this trainer + def name + return pbGetMessageFromHash(MessageTypes::TrainerNames, @real_name) + end + + # @return [String] the translated in-battle lose message of this trainer + def lose_text + return pbGetMessageFromHash(MessageTypes::TrainerLoseText, @real_lose_text) + end + + def replace_species_with_placeholder(species) + case species + when Settings::RIVAL_STARTER_PLACEHOLDER_SPECIES + return pbGet(Settings::RIVAL_STARTER_PLACEHOLDER_VARIABLE) + when Settings::VAR_1_PLACEHOLDER_SPECIES + return pbGet(1) + when Settings::VAR_2_PLACEHOLDER_SPECIES + return pbGet(2) + when Settings::VAR_3_PLACEHOLDER_SPECIES + return pbGet(3) + end + end + + def generateRandomChampionSpecies(old_species) + customsList = getCustomSpeciesList() + bst_range = pbGet(VAR_RANDOMIZER_TRAINER_BST) + new_species = $game_switches[SWITCH_RANDOM_GYM_CUSTOMS] ? getSpecies(getNewCustomSpecies(old_species, customsList, bst_range)) : getSpecies(getNewSpecies(old_species, bst_range)) + #every pokemon should be fully evolved + evolved_species_id = getEvolution(new_species) + evolved_species_id = getEvolution(evolved_species_id) + evolved_species_id = getEvolution(evolved_species_id) + evolved_species_id = getEvolution(evolved_species_id) + return getSpecies(evolved_species_id) + end + + def generateRandomGymSpecies(old_species) + gym_index = pbGet(VAR_CURRENT_GYM_TYPE) + return old_species if gym_index == -1 + return generateRandomChampionSpecies(old_species) if gym_index == 999 + type_id = pbGet(VAR_GYM_TYPES_ARRAY)[gym_index] + return old_species if type_id == -1 + + customsList = getCustomSpeciesList() + bst_range = pbGet(VAR_RANDOMIZER_TRAINER_BST) + gym_type = GameData::Type.get(type_id) + while true + new_species = $game_switches[SWITCH_RANDOM_GYM_CUSTOMS] ? getSpecies(getNewCustomSpecies(old_species, customsList, bst_range)) : getSpecies(getNewSpecies(old_species, bst_range)) + if new_species.hasType?(gym_type) + return new_species + end + end + end + + def replace_species_to_randomized_gym(species, trainerId, pokemonIndex) + if $PokemonGlobal.randomGymTrainersHash == nil + $PokemonGlobal.randomGymTrainersHash = {} + end + if $game_switches[SWITCH_RANDOM_GYM_PERSIST_TEAMS] && $PokemonGlobal.randomGymTrainersHash != nil + if $PokemonGlobal.randomGymTrainersHash[trainerId] != nil && $PokemonGlobal.randomGymTrainersHash[trainerId].length >= $PokemonGlobal.randomTrainersHash[trainerId].length + return getSpecies($PokemonGlobal.randomGymTrainersHash[trainerId][pokemonIndex]) + end + end + new_species = generateRandomGymSpecies(species) + if $game_switches[SWITCH_RANDOM_GYM_PERSIST_TEAMS] + add_generated_species_to_gym_array(new_species, trainerId) + end + return new_species + end + + def add_generated_species_to_gym_array(new_species, trainerId) + if (new_species.is_a?(Symbol)) + id = new_species + else + id = new_species.id_number + end + + expected_team_length = 1 + expected_team_length = $PokemonGlobal.randomTrainersHash[trainerId].length if $PokemonGlobal.randomTrainersHash[trainerId] + new_team = [] + if $PokemonGlobal.randomGymTrainersHash[trainerId] + new_team = $PokemonGlobal.randomGymTrainersHash[trainerId] + end + if new_team.length < expected_team_length + new_team << id + end + $PokemonGlobal.randomGymTrainersHash[trainerId] = new_team + end + + def replace_species_to_randomized_regular(species, trainerId, pokemonIndex) + if $PokemonGlobal.randomTrainersHash[trainerId] == nil + Kernel.pbMessage(_INTL("The trainers need to be re-shuffled.")) + Kernel.pbShuffleTrainers() + end + new_species_dex = $PokemonGlobal.randomTrainersHash[trainerId][pokemonIndex] + return getSpecies(new_species_dex) + end + + def isGymBattle + return ($game_switches[SWITCH_RANDOM_TRAINERS] && ($game_variables[VAR_CURRENT_GYM_TYPE] != -1) || ($game_switches[SWITCH_FIRST_RIVAL_BATTLE] && $game_switches[SWITCH_RANDOM_STARTERS])) + end + + def replace_species_to_randomized(species, trainerId, pokemonIndex) + return species if $game_switches[SWITCH_FIRST_RIVAL_BATTLE] + return species if $game_switches[SWITCH_DONT_RANDOMIZE] + if isGymBattle() && $game_switches[SWITCH_RANDOMIZE_GYMS_SEPARATELY] + return replace_species_to_randomized_gym(species, trainerId, pokemonIndex) + end + return replace_species_to_randomized_regular(species, trainerId, pokemonIndex) + + end + + def replaceSingleSpeciesModeIfApplicable(species) + if $game_switches[SWITCH_SINGLE_POKEMON_MODE] + if $game_switches[SWITCH_SINGLE_POKEMON_MODE_HEAD] + return replaceFusionsHeadWithSpecies(species) + elsif $game_switches[SWITCH_SINGLE_POKEMON_MODE_BODY] + return replaceFusionsBodyWithSpecies(species) + elsif $game_switches[SWITCH_SINGLE_POKEMON_MODE_RANDOM] + if (rand(2) == 0) + return replaceFusionsHeadWithSpecies(species) + else + return replaceFusionsBodyWithSpecies(species) + end + end + end + return species + end + + def replaceFusionsHeadWithSpecies(species) + speciesId = getDexNumberForSpecies(species) + if speciesId > NB_POKEMON + bodyPoke = getBodyID(speciesId) + headPoke = pbGet(VAR_SINGLE_POKEMON_MODE) + newSpecies = bodyPoke * NB_POKEMON + headPoke + return getPokemon(newSpecies) + end + return species + end + + def replaceFusionsBodyWithSpecies(species) + speciesId = getDexNumberForSpecies(species) + if speciesId > NB_POKEMON + bodyPoke = pbGet(VAR_SINGLE_POKEMON_MODE) + headPoke = getHeadID(species) + newSpecies = bodyPoke * NB_POKEMON + headPoke + return getPokemon(newSpecies) + end + return species + end + + def to_trainer + placeholder_species = [Settings::RIVAL_STARTER_PLACEHOLDER_SPECIES, + Settings::VAR_1_PLACEHOLDER_SPECIES, + Settings::VAR_2_PLACEHOLDER_SPECIES, + Settings::VAR_3_PLACEHOLDER_SPECIES] + # Determine trainer's name + tr_name = self.name + Settings::RIVAL_NAMES.each do |rival| + next if rival[0] != @trainer_type || !$game_variables[rival[1]].is_a?(String) + tr_name = $game_variables[rival[1]] + break + end + # Create trainer object + trainer = NPCTrainer.new(tr_name, @trainer_type) + trainer.id = $Trainer.make_foreign_ID + trainer.items = @items.clone + trainer.lose_text = self.lose_text + + isRematch = $game_switches[SWITCH_IS_REMATCH] + isPlayingRandomized = $game_switches[SWITCH_RANDOM_TRAINERS] && !$game_switches[SWITCH_FIRST_RIVAL_BATTLE] + rematchId = getRematchId(trainer.name, trainer.trainer_type) + + # Create each Pokémon owned by the trainer + index = 0 + @pokemon.each do |pkmn_data| + #replace placeholder species infinite fusion edit + species = GameData::Species.get(pkmn_data[:species]).species + original_species = species + if placeholder_species.include?(species) + species = replace_species_with_placeholder(species) + else + species = replace_species_to_randomized(species, self.id, index) if isPlayingRandomized + end + species = replaceSingleSpeciesModeIfApplicable(species) + if $game_switches[SWITCH_REVERSED_MODE] + species = reverseFusionSpecies(species) + end + level = pkmn_data[:level] + if $game_switches[SWITCH_GAME_DIFFICULTY_HARD] + level = (level * Settings::HARD_MODE_LEVEL_MODIFIER).ceil + if level > Settings::MAXIMUM_LEVEL + level = Settings::MAXIMUM_LEVEL + end + end + + if $game_switches[Settings::OVERRIDE_BATTLE_LEVEL_SWITCH] + override_level = $game_variables[Settings::OVERRIDE_BATTLE_LEVEL_VALUE_VAR] + if override_level.is_a?(Integer) + level = override_level + end + end + #### + + #trainer rematch infinite fusion edit + if isRematch + nbRematch = getNumberRematch(rematchId) + level = getRematchLevel(level, nbRematch) + species = evolveRematchPokemon(nbRematch, species) + end + + pkmn = Pokemon.new(species, level, trainer, false) + + trainer.party.push(pkmn) + # Set Pokémon's properties if defined + if pkmn_data[:form] + pkmn.forced_form = pkmn_data[:form] if MultipleForms.hasFunction?(species, "getForm") + pkmn.form_simple = pkmn_data[:form] + end + + if $game_switches[SWITCH_RANDOM_HELD_ITEMS] + pkmn.item = pbGetRandomHeldItem().id + else + pkmn.item = pkmn_data[:item] + end + if pkmn_data[:moves] && pkmn_data[:moves].length > 0 && original_species == species + pkmn_data[:moves].each { |move| pkmn.learn_move(move) } + else + pkmn.reset_moves + end + pkmn.ability_index = pkmn_data[:ability_index] + pkmn.ability = pkmn_data[:ability] + pkmn.gender = pkmn_data[:gender] || ((trainer.male?) ? 0 : 1) + pkmn.shiny = (pkmn_data[:shininess]) ? true : false + if pkmn_data[:nature] + pkmn.nature = pkmn_data[:nature] + else + nature = pkmn.species_data.id_number + GameData::TrainerType.get(trainer.trainer_type).id_number + pkmn.nature = nature % (GameData::Nature::DATA.length / 2) + end + GameData::Stat.each_main do |s| + if pkmn_data[:iv] + pkmn.iv[s.id] = pkmn_data[:iv][s.id] + else + pkmn.iv[s.id] = [pkmn_data[:level] / 2, Pokemon::IV_STAT_LIMIT].min + end + if pkmn_data[:ev] + pkmn.ev[s.id] = pkmn_data[:ev][s.id] + else + pkmn.ev[s.id] = [pkmn_data[:level] * 3 / 2, Pokemon::EV_LIMIT / 6].min + end + end + pkmn.happiness = pkmn_data[:happiness] if pkmn_data[:happiness] + pkmn.name = pkmn_data[:name] if pkmn_data[:name] && !pkmn_data[:name].empty? + if pkmn_data[:shadowness] + pkmn.makeShadow + pkmn.update_shadow_moves(true) + pkmn.shiny = false + end + pkmn.poke_ball = pkmn_data[:poke_ball] if pkmn_data[:poke_ball] + pkmn.calc_stats + + index += 1 + end + return trainer + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbGetTrainerData(tr_type, tr_name, tr_version = 0) + Deprecation.warn_method('pbGetTrainerData', 'v20', 'GameData::Trainer.get(tr_type, tr_name, tr_version)') + return GameData::Trainer.get(tr_type, tr_name, tr_version) +end diff --git a/Data/Scripts/010_Data/002_PBS data/014_Metadata.rb b/Data/Scripts/010_Data/002_PBS data/014_Metadata.rb new file mode 100644 index 000000000..6149254df --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/014_Metadata.rb @@ -0,0 +1,147 @@ +module GameData + class Metadata + attr_reader :id + attr_reader :home + attr_reader :wild_battle_BGM + attr_reader :trainer_battle_BGM + attr_reader :wild_victory_ME + attr_reader :trainer_victory_ME + attr_reader :wild_capture_ME + attr_reader :surf_BGM + attr_reader :bicycle_BGM + attr_reader :player_A + attr_reader :player_B + attr_reader :player_C + attr_reader :player_D + attr_reader :player_E + attr_reader :player_F + attr_reader :player_G + attr_reader :player_H + + DATA = {} + DATA_FILENAME = "metadata.dat" + + SCHEMA = { + "Home" => [1, "vuuu"], + "WildBattleBGM" => [2, "s"], + "TrainerBattleBGM" => [3, "s"], + "WildVictoryME" => [4, "s"], + "TrainerVictoryME" => [5, "s"], + "WildCaptureME" => [6, "s"], + "SurfBGM" => [7, "s"], + "BicycleBGM" => [8, "s"], + "PlayerA" => [9, "esssssss", :TrainerType], + "PlayerB" => [10, "esssssss", :TrainerType], + "PlayerC" => [11, "esssssss", :TrainerType], + "PlayerD" => [12, "esssssss", :TrainerType], + "PlayerE" => [13, "esssssss", :TrainerType], + "PlayerF" => [14, "esssssss", :TrainerType], + "PlayerG" => [15, "esssssss", :TrainerType], + "PlayerH" => [16, "esssssss", :TrainerType] + } + + extend ClassMethodsIDNumbers + include InstanceMethods + + def self.editor_properties + return [ + ["Home", MapCoordsFacingProperty, _INTL("Map ID and X and Y coordinates of where the player goes if no Pokémon Center was entered after a loss.")], + ["WildBattleBGM", BGMProperty, _INTL("Default BGM for wild Pokémon battles.")], + ["TrainerBattleBGM", BGMProperty, _INTL("Default BGM for Trainer battles.")], + ["WildVictoryME", MEProperty, _INTL("Default ME played after winning a wild Pokémon battle.")], + ["TrainerVictoryME", MEProperty, _INTL("Default ME played after winning a Trainer battle.")], + ["WildCaptureME", MEProperty, _INTL("Default ME played after catching a Pokémon.")], + ["SurfBGM", BGMProperty, _INTL("BGM played while surfing.")], + ["BicycleBGM", BGMProperty, _INTL("BGM played while on a bicycle.")], + ["PlayerA", PlayerProperty, _INTL("Specifies player A.")], + ["PlayerB", PlayerProperty, _INTL("Specifies player B.")], + ["PlayerC", PlayerProperty, _INTL("Specifies player C.")], + ["PlayerD", PlayerProperty, _INTL("Specifies player D.")], + ["PlayerE", PlayerProperty, _INTL("Specifies player E.")], + ["PlayerF", PlayerProperty, _INTL("Specifies player F.")], + ["PlayerG", PlayerProperty, _INTL("Specifies player G.")], + ["PlayerH", PlayerProperty, _INTL("Specifies player H.")] + ] + end + + def self.get + return DATA[0] + end + + def self.get_player(id) + return self.get.player_A + case id + when 0 then return self.get.player_A + when 1 then return self.get.player_B + when 2 then return self.get.player_C + when 3 then return self.get.player_D + when 4 then return self.get.player_E + when 5 then return self.get.player_F + when 6 then return self.get.player_G + when 7 then return self.get.player_H + end + return nil + end + + def initialize(hash) + @id = hash[:id] + @home = hash[:home] + @wild_battle_BGM = hash[:wild_battle_BGM] + @trainer_battle_BGM = hash[:trainer_battle_BGM] + @wild_victory_ME = hash[:wild_victory_ME] + @trainer_victory_ME = hash[:trainer_victory_ME] + @wild_capture_ME = hash[:wild_capture_ME] + @surf_BGM = hash[:surf_BGM] + @bicycle_BGM = hash[:bicycle_BGM] + @player_A = hash[:player_A] + @player_B = hash[:player_B] + @player_C = hash[:player_C] + @player_D = hash[:player_D] + @player_E = hash[:player_E] + @player_F = hash[:player_F] + @player_G = hash[:player_G] + @player_H = hash[:player_H] + end + + def property_from_string(str) + case str + when "Home" then return @home + when "WildBattleBGM" then return @wild_battle_BGM + when "TrainerBattleBGM" then return @trainer_battle_BGM + when "WildVictoryME" then return @wild_victory_ME + when "TrainerVictoryME" then return @trainer_victory_ME + when "WildCaptureME" then return @wild_capture_ME + when "SurfBGM" then return @surf_BGM + when "BicycleBGM" then return @bicycle_BGM + when "PlayerA" then return @player_A + when "PlayerB" then return @player_B + when "PlayerC" then return @player_C + when "PlayerD" then return @player_D + when "PlayerE" then return @player_E + when "PlayerF" then return @player_F + when "PlayerG" then return @player_G + when "PlayerH" then return @player_H + end + return nil + end + end +end + +#=============================================================================== +# Deprecated methods +#=============================================================================== +# @deprecated This alias is slated to be removed in v20. +def pbLoadMetadata + Deprecation.warn_method('pbLoadMetadata', 'v20', 'GameData::Metadata.get or GameData::MapMetadata.get(map_id)') + return nil +end + +# @deprecated This alias is slated to be removed in v20. +def pbGetMetadata(map_id, metadata_type) + if map_id == 0 # Global metadata + Deprecation.warn_method('pbGetMetadata', 'v20', 'GameData::Metadata.get.something') + else # Map metadata + Deprecation.warn_method('pbGetMetadata', 'v20', 'GameData::MapMetadata.get(map_id).something') + end + return nil +end diff --git a/Data/Scripts/010_Data/002_PBS data/015_MapMetadata.rb b/Data/Scripts/010_Data/002_PBS data/015_MapMetadata.rb new file mode 100644 index 000000000..f3dc57946 --- /dev/null +++ b/Data/Scripts/010_Data/002_PBS data/015_MapMetadata.rb @@ -0,0 +1,129 @@ +module GameData + class MapMetadata + attr_reader :id + attr_reader :outdoor_map + attr_reader :announce_location + attr_reader :can_bicycle + attr_reader :always_bicycle + attr_reader :teleport_destination + attr_reader :weather + attr_reader :town_map_position + attr_reader :dive_map_id + attr_reader :dark_map + attr_reader :safari_map + attr_reader :snap_edges + attr_reader :random_dungeon + attr_reader :battle_background + attr_reader :wild_battle_BGM + attr_reader :trainer_battle_BGM + attr_reader :wild_victory_ME + attr_reader :trainer_victory_ME + attr_reader :wild_capture_ME + attr_reader :town_map_size + attr_reader :battle_environment + + DATA = {} + DATA_FILENAME = "map_metadata.dat" + + SCHEMA = { + "Outdoor" => [1, "b"], + "ShowArea" => [2, "b"], + "Bicycle" => [3, "b"], + "BicycleAlways" => [4, "b"], + "HealingSpot" => [5, "vuu"], + "Weather" => [6, "eu", :Weather], + "MapPosition" => [7, "uuu"], + "DiveMap" => [8, "v"], + "DarkMap" => [9, "b"], + "SafariMap" => [10, "b"], + "SnapEdges" => [11, "b"], + "Dungeon" => [12, "b"], + "BattleBack" => [13, "s"], + "WildBattleBGM" => [14, "s"], + "TrainerBattleBGM" => [15, "s"], + "WildVictoryME" => [16, "s"], + "TrainerVictoryME" => [17, "s"], + "WildCaptureME" => [18, "s"], + "MapSize" => [19, "us"], + "Environment" => [20, "e", :Environment] + } + + extend ClassMethodsIDNumbers + include InstanceMethods + + def self.editor_properties + return [ + ["Outdoor", BooleanProperty, _INTL("If true, this map is an outdoor map and will be tinted according to time of day.")], + ["ShowArea", BooleanProperty, _INTL("If true, the game will display the map's name upon entry.")], + ["Bicycle", BooleanProperty, _INTL("If true, the bicycle can be used on this map.")], + ["BicycleAlways", BooleanProperty, _INTL("If true, the bicycle will be mounted automatically on this map and cannot be dismounted.")], + ["HealingSpot", MapCoordsProperty, _INTL("Map ID of this Pokémon Center's town, and X and Y coordinates of its entrance within that town.")], + ["Weather", WeatherEffectProperty, _INTL("Weather conditions in effect for this map.")], + ["MapPosition", RegionMapCoordsProperty, _INTL("Identifies the point on the regional map for this map.")], + ["DiveMap", MapProperty, _INTL("Specifies the underwater layer of this map. Use only if this map has deep water.")], + ["DarkMap", BooleanProperty, _INTL("If true, this map is dark and a circle of light appears around the player. Flash can be used to expand the circle.")], + ["SafariMap", BooleanProperty, _INTL("If true, this map is part of the Safari Zone (both indoor and outdoor). Not to be used in the reception desk.")], + ["SnapEdges", BooleanProperty, _INTL("If true, when the player goes near this map's edge, the game doesn't center the player as usual.")], + ["Dungeon", BooleanProperty, _INTL("If true, this map has a randomly generated layout. See the wiki for more information.")], + ["BattleBack", StringProperty, _INTL("PNG files named 'XXX_bg', 'XXX_base0', 'XXX_base1', 'XXX_message' in Battlebacks folder, where XXX is this property's value.")], + ["WildBattleBGM", BGMProperty, _INTL("Default BGM for wild Pokémon battles on this map.")], + ["TrainerBattleBGM", BGMProperty, _INTL("Default BGM for trainer battles on this map.")], + ["WildVictoryME", MEProperty, _INTL("Default ME played after winning a wild Pokémon battle on this map.")], + ["TrainerVictoryME", MEProperty, _INTL("Default ME played after winning a Trainer battle on this map.")], + ["WildCaptureME", MEProperty, _INTL("Default ME played after catching a wild Pokémon on this map.")], + ["MapSize", MapSizeProperty, _INTL("The width of the map in Town Map squares, and a string indicating which squares are part of this map.")], + ["Environment", GameDataProperty.new(:Environment), _INTL("The default battle environment for battles on this map.")] + ] + end + + def initialize(hash) + @id = hash[:id] + @outdoor_map = hash[:outdoor_map] + @announce_location = hash[:announce_location] + @can_bicycle = hash[:can_bicycle] + @always_bicycle = hash[:always_bicycle] + @teleport_destination = hash[:teleport_destination] + @weather = hash[:weather] + @town_map_position = hash[:town_map_position] + @dive_map_id = hash[:dive_map_id] + @dark_map = hash[:dark_map] + @safari_map = hash[:safari_map] + @snap_edges = hash[:snap_edges] + @random_dungeon = hash[:random_dungeon] + @battle_background = hash[:battle_background] + @wild_battle_BGM = hash[:wild_battle_BGM] + @trainer_battle_BGM = hash[:trainer_battle_BGM] + @wild_victory_ME = hash[:wild_victory_ME] + @trainer_victory_ME = hash[:trainer_victory_ME] + @wild_capture_ME = hash[:wild_capture_ME] + @town_map_size = hash[:town_map_size] + @battle_environment = hash[:battle_environment] + end + + def property_from_string(str) + case str + when "Outdoor" then return @outdoor_map + when "ShowArea" then return @announce_location + when "Bicycle" then return @can_bicycle + when "BicycleAlways" then return @always_bicycle + when "HealingSpot" then return @teleport_destination + when "Weather" then return @weather + when "MapPosition" then return @town_map_position + when "DiveMap" then return @dive_map_id + when "DarkMap" then return @dark_map + when "SafariMap" then return @safari_map + when "SnapEdges" then return @snap_edges + when "Dungeon" then return @random_dungeon + when "BattleBack" then return @battle_background + when "WildBattleBGM" then return @wild_battle_BGM + when "TrainerBattleBGM" then return @trainer_battle_BGM + when "WildVictoryME" then return @wild_victory_ME + when "TrainerVictoryME" then return @trainer_victory_ME + when "WildCaptureME" then return @wild_capture_ME + when "MapSize" then return @town_map_size + when "Environment" then return @battle_environment + end + return nil + end + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/001_PokeBattle_Battler.rb b/Data/Scripts/011_Battle/001_Battler/001_PokeBattle_Battler.rb new file mode 100644 index 000000000..20172f0ec --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/001_PokeBattle_Battler.rb @@ -0,0 +1,801 @@ +class PokeBattle_Battler + # Fundamental to this object + attr_reader :battle + attr_accessor :index + # The Pokémon and its properties + attr_reader :pokemon + attr_accessor :pokemonIndex + attr_accessor :species + attr_accessor :type1 + attr_accessor :type2 + attr_accessor :ability_id + attr_accessor :item_id + attr_accessor :moves + attr_accessor :gender + attr_accessor :iv + attr_accessor :attack + attr_accessor :spatk + attr_accessor :speed + attr_accessor :stages + attr_reader :totalhp + attr_reader :fainted # Boolean to mark whether self has fainted properly + attr_accessor :captured # Boolean to mark whether self was captured + attr_reader :dummy + attr_accessor :effects + # Things the battler has done in battle + attr_accessor :turnCount + attr_accessor :participants + attr_accessor :lastAttacker + attr_accessor :lastFoeAttacker + attr_accessor :lastHPLost + attr_accessor :lastHPLostFromFoe + attr_accessor :lastMoveUsed + attr_accessor :lastMoveUsedType + attr_accessor :lastRegularMoveUsed + attr_accessor :lastRegularMoveTarget # For Instruct + attr_accessor :lastRoundMoved + attr_accessor :lastMoveFailed # For Stomping Tantrum + attr_accessor :lastRoundMoveFailed # For Stomping Tantrum + attr_accessor :movesUsed + attr_accessor :currentMove # ID of multi-turn move currently being used + attr_accessor :tookDamage # Boolean for whether self took damage this round + attr_accessor :tookPhysicalHit + attr_accessor :damageState + attr_accessor :initialHP # Set at the start of each move's usage + + #============================================================================= + # Complex accessors + #============================================================================= + attr_reader :level + + def level=(value) + @level = value + @pokemon.level = value if @pokemon + end + + attr_reader :form + + def form=(value) + @form = value + @pokemon.form = value if @pokemon + end + + def ability + return GameData::Ability.try_get(@ability_id) + end + + def hasHiddenAbility? + return @pokemon.ability_index >= 2 + end + + def ability=(value) + new_ability = GameData::Ability.try_get(value) + @ability_id = (new_ability) ? new_ability.id : nil + end + + def item + return GameData::Item.try_get(@item_id) + end + + def item=(value) + new_item = GameData::Item.try_get(value) + @item_id = (new_item) ? new_item.id : nil + @pokemon.item = @item_id if @pokemon + end + + def defense + return @spdef if @battle.field.effects[PBEffects::WonderRoom] > 0 + return @defense + end + + attr_writer :defense + + def spdef + return @defense if @battle.field.effects[PBEffects::WonderRoom] > 0 + return @spdef + end + + attr_writer :spdef + + attr_reader :hp + + def hp=(value) + checkHPRelatedFormChange(value) #careful, setting @pokemon.hp also calls a method for changing the form on hp change so we should not change the form here, just update the graphics + @hp = value.to_i + @pokemon.hp = value.to_i if @pokemon + end + + def fainted? + return @hp <= 0; + end + + alias isFainted? fainted? + + attr_reader :status + + def status=(value) + @effects[PBEffects::Truant] = false if @status == :SLEEP && value != :SLEEP + @effects[PBEffects::Toxic] = 0 if value != :POISON + @status = value + @pokemon.status = value if @pokemon + self.statusCount = 0 if value != :POISON && value != :SLEEP + @battle.scene.pbRefreshOne(@index) + end + + attr_reader :statusCount + + def statusCount=(value) + @statusCount = value + @pokemon.statusCount = value if @pokemon + @battle.scene.pbRefreshOne(@index) + end + + #============================================================================= + # Properties from Pokémon + #============================================================================= + def happiness + return @pokemon ? @pokemon.happiness : 0; + end + + def nature + return @pokemon ? @pokemon.nature : 0; + end + + def pokerusStage + return @pokemon ? @pokemon.pokerusStage : 0; + end + + #============================================================================= + # Mega Evolution, Primal Reversion, Shadow Pokémon + #============================================================================= + def hasMega? + return false if @effects[PBEffects::Transform] + return @pokemon && @pokemon.hasMegaForm? + end + + def mega? + return @pokemon && @pokemon.mega?; + end + + alias isMega? mega? + + def hasPrimal? + return false if @effects[PBEffects::Transform] + return @pokemon && @pokemon.hasPrimalForm? + end + + def primal? + return @pokemon && @pokemon.primal?; + end + + alias isPrimal? primal? + + def shadowPokemon? + return false; + end + + alias isShadow? shadowPokemon? + + def inHyperMode? + return false; + end + + #============================================================================= + # Display-only properties + #============================================================================= + def name + return @effects[PBEffects::Illusion].name if @effects[PBEffects::Illusion] + return @name + end + + attr_writer :name + + def displayPokemon + return @effects[PBEffects::Illusion] if @effects[PBEffects::Illusion] + return self.pokemon + end + + def displaySpecies + return @effects[PBEffects::Illusion].species if @effects[PBEffects::Illusion] + return self.species + end + + def displayGender + return @effects[PBEffects::Illusion].gender if @effects[PBEffects::Illusion] + return self.gender + end + + def displayForm + return @effects[PBEffects::Illusion].form if @effects[PBEffects::Illusion] + return self.form + end + + def shiny? + return @effects[PBEffects::Illusion].shiny? if @effects[PBEffects::Illusion] + return @pokemon && @pokemon.shiny? + end + + alias isShiny? shiny? + + def glitter? + return @pokemon.glitter + end + + def owned? + return false if !@battle.wildBattle? + return $Trainer.owned?(displaySpecies) + end + + alias owned owned? + + def abilityName + abil = self.ability + return (abil) ? abil.name : "" + end + + def itemName + itm = self.item + return (itm) ? itm.name : "" + end + + def pbThis(lowerCase = false) + if opposes? + if @battle.trainerBattle? + return lowerCase ? _INTL("the opposing {1}", name) : _INTL("The opposing {1}", name) + else + return lowerCase ? _INTL("the wild {1}", name) : _INTL("The wild {1}", name) + end + elsif !pbOwnedByPlayer? + return lowerCase ? _INTL("the ally {1}", name) : _INTL("The ally {1}", name) + end + return name + end + + def pbTeam(lowerCase = false) + if opposes? + return lowerCase ? _INTL("the opposing team") : _INTL("The opposing team") + end + return lowerCase ? _INTL("your team") : _INTL("Your team") + end + + def pbOpposingTeam(lowerCase = false) + if opposes? + return lowerCase ? _INTL("your team") : _INTL("Your team") + end + return lowerCase ? _INTL("the opposing team") : _INTL("The opposing team") + end + + #============================================================================= + # Calculated properties + #============================================================================= + def pbSpeed + return 1 if fainted? + stageMul = [2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8] + stageDiv = [8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2] + stage = @stages[:SPEED] + 6 + speed = @speed * stageMul[stage] / stageDiv[stage] + speedMult = 1.0 + # Ability effects that alter calculated Speed + if abilityActive? + speedMult = BattleHandlers.triggerSpeedCalcAbility(self.ability, self, speedMult) + end + # Item effects that alter calculated Speed + if itemActive? + speedMult = BattleHandlers.triggerSpeedCalcItem(self.item, self, speedMult) + end + # Other effects + speedMult *= 2 if pbOwnSide.effects[PBEffects::Tailwind] > 0 + speedMult /= 2 if pbOwnSide.effects[PBEffects::Swamp] > 0 + # Paralysis + if status == :PARALYSIS && !hasActiveAbility?(:QUICKFEET) + speedMult /= (Settings::MECHANICS_GENERATION >= 7) ? 2 : 4 + end + # Badge multiplier + if @battle.internalBattle && pbOwnedByPlayer? && + @battle.pbPlayer.badge_count >= Settings::NUM_BADGES_BOOST_SPEED + speedMult *= 1.1 + end + # Calculation + return [(speed * speedMult).round, 1].max + end + + def pbWeight + ret = (@pokemon) ? @pokemon.weight : 500 + ret += @effects[PBEffects::WeightChange] + ret = 1 if ret < 1 + if abilityActive? && !@battle.moldBreaker + ret = BattleHandlers.triggerWeightCalcAbility(self.ability, self, ret) + end + if itemActive? + ret = BattleHandlers.triggerWeightCalcItem(self.item, self, ret) + end + return [ret, 1].max + end + + #============================================================================= + # Queries about what the battler has + #============================================================================= + def plainStats + ret = {} + ret[:ATTACK] = self.attack + ret[:DEFENSE] = self.defense + ret[:SPECIAL_ATTACK] = self.spatk + ret[:SPECIAL_DEFENSE] = self.spdef + ret[:SPEED] = self.speed + return ret + end + + def isSpecies?(species) + return @pokemon && @pokemon.isSpecies?(species) + end + + def hasBodyOf?(check_species) + return @pokemon.hasBodyOf?(check_species) + end + + def hasHeadOf?(check_species) + return @pokemon.hasHeadOf?(check_species) + end + + def isFusionOf(check_species) + return @pokemon.isFusionOf(check_species) + end + + def isFusion?() + return @pokemon.isFusion?() + end + + # Returns the active types of this Pokémon. The array should not include the + # same type more than once, and should not include any invalid type numbers + # (e.g. -1). + def pbTypes(withType3 = false) + ret = [@type1] + ret.push(@type2) if @type2 != @type1 + # Burn Up erases the Fire-type. + ret.delete(:FIRE) if @effects[PBEffects::BurnUp] + # Roost erases the Flying-type. If there are no types left, adds the Normal- + # type. + if @effects[PBEffects::Roost] + ret.delete(:FLYING) + ret.push(:NORMAL) if ret.length == 0 + end + # Add the third type specially. + if withType3 && @effects[PBEffects::Type3] + ret.push(@effects[PBEffects::Type3]) if !ret.include?(@effects[PBEffects::Type3]) + end + return ret + end + + def pbHasType?(type) + return false if !type + activeTypes = pbTypes(true) + return activeTypes.include?(GameData::Type.get(type).id) + end + + def pbHasOtherType?(type) + return false if !type + activeTypes = pbTypes(true) + activeTypes.delete(GameData::Type.get(type).id) + return activeTypes.length > 0 + end + + # NOTE: Do not create any held item which affects whether a Pokémon's ability + # is active. The ability Klutz affects whether a Pokémon's item is + # active, and the code for the two combined would cause an infinite loop + # (regardless of whether any Pokémon actualy has either the ability or + # the item - the code existing is enough to cause the loop). + def abilityActive?(ignore_fainted = false) + return false if fainted? && !ignore_fainted + return false if @effects[PBEffects::GastroAcid] + return true + end + + def hasActiveAbility?(check_ability, ignore_fainted = false) + return false if !abilityActive?(ignore_fainted) + return check_ability.include?(@ability_id) if check_ability.is_a?(Array) + return self.ability == check_ability + end + + alias hasWorkingAbility hasActiveAbility? + + # Applies to both losing self's ability (i.e. being replaced by another) and + # having self's ability be negated. + def unstoppableAbility?(abil = nil) + abil = @ability_id if !abil + abil = GameData::Ability.try_get(abil) + return false if !abil + ability_blacklist = [ + # Form-changing abilities + :BATTLEBOND, + :DISGUISE, + # :FLOWERGIFT, # This can be stopped + # :FORECAST, # This can be stopped + :MULTITYPE, + :POWERCONSTRUCT, + :SCHOOLING, + :SHIELDSDOWN, + :STANCECHANGE, + :ZENMODE, + # Abilities intended to be inherent properties of a certain species + :COMATOSE, + :RKSSYSTEM + ] + return ability_blacklist.include?(abil.id) + end + + # Applies to gaining the ability. + def ungainableAbility?(abil = nil) + abil = @ability_id if !abil + abil = GameData::Ability.try_get(abil) + return false if !abil + ability_blacklist = [ + # Form-changing abilities + :BATTLEBOND, + :DISGUISE, + :FLOWERGIFT, + :FORECAST, + :MULTITYPE, + :POWERCONSTRUCT, + :SCHOOLING, + :SHIELDSDOWN, + :STANCECHANGE, + :ZENMODE, + # Appearance-changing abilities + :ILLUSION, + :IMPOSTER, + # Abilities intended to be inherent properties of a certain species + :COMATOSE, + :RKSSYSTEM + ] + return ability_blacklist.include?(abil.id) + end + + def itemActive?(ignoreFainted = false) + return false if fainted? && !ignoreFainted + return false if @effects[PBEffects::Embargo] > 0 + return false if @battle.field.effects[PBEffects::MagicRoom] > 0 + return false if hasActiveAbility?(:KLUTZ, ignoreFainted) + return true + end + + def hasActiveItem?(check_item, ignore_fainted = false) + return false if !itemActive?(ignore_fainted) + return check_item.include?(@item_id) if check_item.is_a?(Array) + return self.item == check_item + end + + alias hasWorkingItem hasActiveItem? + + # Returns whether the specified item will be unlosable for this Pokémon. + def unlosableItem?(check_item) + return false if !check_item + return true if GameData::Item.get(check_item).is_mail? + return false if @effects[PBEffects::Transform] + # Items that change a Pokémon's form + if mega? # Check if item was needed for this Mega Evolution + return true if @pokemon.species_data.mega_stone == check_item + else + # Check if item could cause a Mega Evolution + GameData::Species.each do |data| + next if data.species != @species || data.unmega_form != @form + return true if data.mega_stone == check_item + end + end + # Other unlosable items + return GameData::Item.get(check_item).unlosable?(@species, self.ability) + end + + def eachMove + @moves.each { |m| yield m } + end + + def eachMoveWithIndex + @moves.each_with_index { |m, i| yield m, i } + end + + def pbHasMove?(move_id) + return false if !move_id + eachMove { |m| return true if m.id == move_id } + return false + end + + def pbHasMoveType?(check_type) + return false if !check_type + check_type = GameData::Type.get(check_type).id + eachMove { |m| return true if m.type == check_type } + return false + end + + def pbHasMoveFunction?(*arg) + return false if !arg + eachMove do |m| + arg.each { |code| return true if m.function == code } + end + return false + end + + def hasMoldBreaker? + return hasActiveAbility?([:MOLDBREAKER, :TERAVOLT, :TURBOBLAZE]) + end + + def canChangeType? + return ![:MULTITYPE, :RKSSYSTEM].include?(@ability_id) + end + + def airborne? + return false if hasActiveItem?(:IRONBALL) + return false if @effects[PBEffects::Ingrain] + return false if @effects[PBEffects::SmackDown] + return false if @battle.field.effects[PBEffects::Gravity] > 0 + return true if pbHasType?(:FLYING) + return true if hasActiveAbility?(:LEVITATE) && !@battle.moldBreaker + return true if hasActiveItem?(:AIRBALLOON) + return true if @effects[PBEffects::MagnetRise] > 0 + return true if @effects[PBEffects::Telekinesis] > 0 + return false + end + + def affectedByTerrain? + return false if airborne? + return false if semiInvulnerable? + return true + end + + def takesIndirectDamage?(showMsg = false) + return false if fainted? + if hasActiveAbility?(:MAGICGUARD) + if showMsg + @battle.pbShowAbilitySplash(self) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!", pbThis)) + else + @battle.pbDisplay(_INTL("{1} is unaffected because of its {2}!", pbThis, abilityName)) + end + @battle.pbHideAbilitySplash(self) + end + return false + end + return true + end + + def takesSandstormDamage? + return false if !takesIndirectDamage? + return false if pbHasType?(:GROUND) || pbHasType?(:ROCK) || pbHasType?(:STEEL) + return false if inTwoTurnAttack?("0CA", "0CB") # Dig, Dive + return false if hasActiveAbility?([:OVERCOAT, :SANDFORCE, :SANDRUSH, :SANDVEIL]) + return false if hasActiveItem?(:SAFETYGOGGLES) + return true + end + + def takesHailDamage? + return false if !takesIndirectDamage? + return false if pbHasType?(:ICE) + return false if inTwoTurnAttack?("0CA", "0CB") # Dig, Dive + return false if hasActiveAbility?([:OVERCOAT, :ICEBODY, :SNOWCLOAK]) + return false if hasActiveItem?(:SAFETYGOGGLES) + return true + end + + def takesShadowSkyDamage? + return false if fainted? + return false if shadowPokemon? + return true + end + + def affectedByPowder?(showMsg = false) + return false if fainted? + if pbHasType?(:GRASS) && Settings::MORE_TYPE_EFFECTS + @battle.pbDisplay(_INTL("{1} is unaffected!", pbThis)) if showMsg + return false + end + if Settings::MECHANICS_GENERATION >= 6 + if hasActiveAbility?(:OVERCOAT) && !@battle.moldBreaker + if showMsg + @battle.pbShowAbilitySplash(self) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!", pbThis)) + else + @battle.pbDisplay(_INTL("{1} is unaffected because of its {2}!", pbThis, abilityName)) + end + @battle.pbHideAbilitySplash(self) + end + return false + end + if hasActiveItem?(:SAFETYGOGGLES) + if showMsg + @battle.pbDisplay(_INTL("{1} is unaffected because of its {2}!", pbThis, itemName)) + end + return false + end + end + return true + end + + def canHeal? + return false if fainted? || @hp >= @totalhp + return false if @effects[PBEffects::HealBlock] > 0 + return true + end + + def affectedByContactEffect?(showMsg = false) + return false if fainted? + if hasActiveItem?(:PROTECTIVEPADS) + @battle.pbDisplay(_INTL("{1} protected itself with the {2}!", pbThis, itemName)) if showMsg + return false + end + return true + end + + def movedThisRound? + return @lastRoundMoved && @lastRoundMoved == @battle.turnCount + end + + def usingMultiTurnAttack? + return true if @effects[PBEffects::TwoTurnAttack] + return true if @effects[PBEffects::HyperBeam] > 0 + return true if @effects[PBEffects::Rollout] > 0 + return true if @effects[PBEffects::Outrage] > 0 + return true if @effects[PBEffects::Uproar] > 0 + return true if @effects[PBEffects::Bide] > 0 + return false + end + + def inTwoTurnAttack?(*arg) + return false if !@effects[PBEffects::TwoTurnAttack] + ttaFunction = GameData::Move.get(@effects[PBEffects::TwoTurnAttack]).function_code + arg.each { |a| return true if a == ttaFunction } + return false + end + + def semiInvulnerable? + return inTwoTurnAttack?("0C9", "0CA", "0CB", "0CC", "0CD", "0CE", "14D") + end + + def pbEncoredMoveIndex + return -1 if @effects[PBEffects::Encore] == 0 || !@effects[PBEffects::EncoreMove] + ret = -1 + eachMoveWithIndex do |m, i| + next if m.id != @effects[PBEffects::EncoreMove] + ret = i + break + end + return ret + end + + def initialItem + return @battle.initialItems[@index & 1][@pokemonIndex] + end + + def setInitialItem(newItem) + @battle.initialItems[@index & 1][@pokemonIndex] = newItem + end + + def recycleItem + return @battle.recycleItems[@index & 1][@pokemonIndex] + end + + def setRecycleItem(newItem) + @battle.recycleItems[@index & 1][@pokemonIndex] = newItem + end + + def belched? + return @battle.belch[@index & 1][@pokemonIndex] + end + + def setBelched + @battle.belch[@index & 1][@pokemonIndex] = true + end + + #============================================================================= + # Methods relating to this battler's position on the battlefield + #============================================================================= + # Returns whether the given position belongs to the opposing Pokémon's side. + def opposes?(i = 0) + i = i.index if i.respond_to?("index") + return (@index & 1) != (i & 1) + end + + # Returns whether the given position/battler is near to self. + def near?(i) + i = i.index if i.respond_to?("index") + return @battle.nearBattlers?(@index, i) + end + + # Returns whether self is owned by the player. + def pbOwnedByPlayer? + return @battle.pbOwnedByPlayer?(@index) + end + + # Returns 0 if self is on the player's side, or 1 if self is on the opposing + # side. + def idxOwnSide + return @index & 1 + end + + # Returns 1 if self is on the player's side, or 0 if self is on the opposing + # side. + def idxOpposingSide + return (@index & 1) ^ 1 + end + + # Returns the data structure for this battler's side. + def pbOwnSide + return @battle.sides[idxOwnSide] + end + + # Returns the data structure for the opposing Pokémon's side. + def pbOpposingSide + return @battle.sides[idxOpposingSide] + end + + # Yields each unfainted ally Pokémon. + def eachAlly + @battle.battlers.each do |b| + yield b if b && !b.fainted? && !b.opposes?(@index) && b.index != @index + end + end + + # Yields each unfainted opposing Pokémon. + def eachOpposing + @battle.battlers.each { |b| yield b if b && !b.fainted? && b.opposes?(@index) } + end + + # Returns the battler that is most directly opposite to self. unfaintedOnly is + # whether it should prefer to return a non-fainted battler. + def pbDirectOpposing(unfaintedOnly = false) + @battle.pbGetOpposingIndicesInOrder(@index).each do |i| + next if !@battle.battlers[i] + break if unfaintedOnly && @battle.battlers[i].fainted? + return @battle.battlers[i] + end + # Wanted an unfainted battler but couldn't find one; make do with a fainted + # battler + @battle.pbGetOpposingIndicesInOrder(@index).each do |i| + return @battle.battlers[i] if @battle.battlers[i] + end + return @battle.battlers[(@index ^ 1)] + end + + #Changes the form VISUALLY in battles. The species is changed in the equivalent method in Pokemon class + def checkHPRelatedFormChange(new_hp) + if @ability_id == :SHIELDSDOWN + if @pokemon.isFusionOf(:MINIOR_M) + if new_hp <= (@totalhp / 2) + changeBattlerForm(:MINIOR_M, :MINIOR_C,nil, :SHELLSMASH) + @battle.pbDisplay(_INTL("{1} changed to the Core Form!", pbThis)) + end + end + if @pokemon.isFusionOf(:MINIOR_C) + if new_hp > (@totalhp / 2) + changeBattlerForm(:MINIOR_C, :MINIOR_M,nil, :SHELLSMASH) + @battle.pbDisplay(_INTL("{1} changed to the Meteor Form!", pbThis)) + end + end + end + end + + + def changeBattlerForm(oldForm, newForm,commonAnimation=nil,moveAnimation=nil) + @pokemon.changeFormSpecies(oldForm, newForm) + if moveAnimation + changeFormSpeciesMoveAnimation(oldForm, newForm,moveAnimation) + else + changeFormSpeciesCommonAnimation(oldForm, newForm, commonAnimation) + end + end + + + #These methods only play the animation and change the graphics. + # To also change the species, call changeFormSpecies() instead + def changeFormSpeciesCommonAnimation(oldForm, newForm, animation = "UltraBurst2") + @battle.scene.pbChangePokemon(self, @pokemon) + @battle.scene.pbCommonAnimation(animation, self) + @battle.scene.pbRefreshOne(@index) + end + + def changeFormSpeciesMoveAnimation(oldForm, newForm, moveID=:REFRESH) + @battle.scene.pbChangePokemon(self, @pokemon) + @battle.scene.pbAnimation(moveID, self,self) + @battle.scene.pbRefreshOne(@index) + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/002_Battler_Initialize.rb b/Data/Scripts/011_Battle/001_Battler/002_Battler_Initialize.rb new file mode 100644 index 000000000..cd05c59e1 --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/002_Battler_Initialize.rb @@ -0,0 +1,330 @@ +class PokeBattle_Battler + #============================================================================= + # Creating a battler + #============================================================================= + def initialize(btl,idxBattler) + @battle = btl + @index = idxBattler + @captured = false + @dummy = false + @stages = {} + @effects = [] + @damageState = PokeBattle_DamageState.new + pbInitBlank + pbInitEffects(false) + end + + def pbInitBlank + @name = "" + @species = 0 + @form = 0 + @level = 0 + @hp = @totalhp = 0 + @type1 = @type2 = nil + @ability_id = nil + @ability2_id = nil + @item_id = nil + @gender = 0 + @attack = @defense = @spatk = @spdef = @speed = 0 + @status = :NONE + @statusCount = 0 + @pokemon = nil + @pokemonIndex = -1 + @participants = [] + @moves = [] + @iv = {} + GameData::Stat.each_main { |s| @iv[s.id] = 0 } + end + + # Used by Future Sight only, when Future Sight's user is no longer in battle. + def pbInitDummyPokemon(pkmn,idxParty) + raise _INTL("An egg can't be an active Pokémon.") if pkmn.egg? + @name = pkmn.name + @species = pkmn.species + @form = pkmn.form + @level = pkmn.level + @hp = pkmn.hp + @totalhp = pkmn.totalhp + @type1 = pkmn.type1 + @type2 = pkmn.type2 + # ability and item intentionally not copied across here + @gender = pkmn.gender + @attack = pkmn.attack + @defense = pkmn.defense + @spatk = pkmn.spatk + @spdef = pkmn.spdef + @speed = pkmn.speed + @status = pkmn.status + @statusCount = pkmn.statusCount + @pokemon = pkmn + @pokemonIndex = idxParty + @participants = [] + # moves intentionally not copied across here + @iv = {} + GameData::Stat.each_main { |s| @iv[s.id] = pkmn.iv[s.id] } + @dummy = true + end + + def pbInitialize(pkmn,idxParty,batonPass=false) + pbInitPokemon(pkmn,idxParty) + pbInitEffects(batonPass) + @damageState.reset + end + + def pbInitPokemon(pkmn,idxParty) + raise _INTL("An egg can't be an active Pokémon.") if pkmn.egg? + @name = pkmn.name + @species = pkmn.species + @form = pkmn.form + @level = pkmn.level + @hp = pkmn.hp + @totalhp = pkmn.totalhp + @type1 = pkmn.type1 + @type2 = pkmn.type2 + @ability_id = pkmn.ability_id + @ability2_id = pkmn.ability2_id + @item_id = pkmn.item_id + @gender = pkmn.gender + @attack = pkmn.attack + @defense = pkmn.defense + @spatk = pkmn.spatk + @spdef = pkmn.spdef + @speed = pkmn.speed + @status = pkmn.status + @statusCount = pkmn.statusCount + @pokemon = pkmn + @pokemonIndex = idxParty + @participants = [] # Participants earn Exp. if this battler is defeated + @moves = [] + pkmn.moves.each_with_index do |m,i| + @moves[i] = PokeBattle_Move.from_pokemon_move(@battle,m) + end + @iv = {} + GameData::Stat.each_main { |s| @iv[s.id] = pkmn.iv[s.id] } + end + + def pbInitEffects(batonPass) + if batonPass + # These effects are passed on if Baton Pass is used, but they need to be + # reapplied + @effects[PBEffects::LaserFocus] = (@effects[PBEffects::LaserFocus]>0) ? 2 : 0 + @effects[PBEffects::LockOn] = (@effects[PBEffects::LockOn]>0) ? 2 : 0 + if @effects[PBEffects::PowerTrick] + @attack,@defense = @defense,@attack + end + # These effects are passed on if Baton Pass is used, but they need to be + # cancelled in certain circumstances anyway + @effects[PBEffects::Telekinesis] = 0 if isSpecies?(:GENGAR) && mega? + @effects[PBEffects::GastroAcid] = false if unstoppableAbility? + else + # These effects are passed on if Baton Pass is used + @stages[:ATTACK] = 0 + @stages[:DEFENSE] = 0 + @stages[:SPEED] = 0 + @stages[:SPECIAL_ATTACK] = 0 + @stages[:SPECIAL_DEFENSE] = 0 + @stages[:ACCURACY] = 0 + @stages[:EVASION] = 0 + @effects[PBEffects::AquaRing] = false + @effects[PBEffects::Confusion] = 0 + @effects[PBEffects::Curse] = false + @effects[PBEffects::Embargo] = 0 + @effects[PBEffects::FocusEnergy] = 0 + @effects[PBEffects::GastroAcid] = false + @effects[PBEffects::HealBlock] = 0 + @effects[PBEffects::Ingrain] = false + @effects[PBEffects::LaserFocus] = 0 + @effects[PBEffects::LeechSeed] = -1 + @effects[PBEffects::LockOn] = 0 + @effects[PBEffects::LockOnPos] = -1 + @effects[PBEffects::MagnetRise] = 0 + @effects[PBEffects::PerishSong] = 0 + @effects[PBEffects::PerishSongUser] = -1 + @effects[PBEffects::PowerTrick] = false + @effects[PBEffects::Substitute] = 0 + @effects[PBEffects::Telekinesis] = 0 + end + @fainted = (@hp==0) + @initialHP = 0 + @lastAttacker = [] + @lastFoeAttacker = [] + @lastHPLost = 0 + @lastHPLostFromFoe = 0 + @tookDamage = false + @tookPhysicalHit = false + @lastMoveUsed = nil + @lastMoveUsedType = nil + @lastRegularMoveUsed = nil + @lastRegularMoveTarget = -1 + @lastRoundMoved = -1 + @lastMoveFailed = false + @lastRoundMoveFailed = false + @movesUsed = [] + @turnCount = 0 + @effects[PBEffects::Attract] = -1 + @battle.eachBattler do |b| # Other battlers no longer attracted to self + b.effects[PBEffects::Attract] = -1 if b.effects[PBEffects::Attract]==@index + end + @effects[PBEffects::BanefulBunker] = false + @effects[PBEffects::BeakBlast] = false + @effects[PBEffects::Bide] = 0 + @effects[PBEffects::BideDamage] = 0 + @effects[PBEffects::BideTarget] = -1 + @effects[PBEffects::BurnUp] = false + @effects[PBEffects::Charge] = 0 + @effects[PBEffects::ChoiceBand] = nil + @effects[PBEffects::Counter] = -1 + @effects[PBEffects::CounterTarget] = -1 + @effects[PBEffects::Dancer] = false + @effects[PBEffects::DefenseCurl] = false + @effects[PBEffects::DestinyBond] = false + @effects[PBEffects::DestinyBondPrevious] = false + @effects[PBEffects::DestinyBondTarget] = -1 + @effects[PBEffects::Disable] = 0 + @effects[PBEffects::DisableMove] = nil + @effects[PBEffects::Electrify] = false + @effects[PBEffects::Encore] = 0 + @effects[PBEffects::EncoreMove] = nil + @effects[PBEffects::Endure] = false + @effects[PBEffects::FirstPledge] = 0 + @effects[PBEffects::FlashFire] = false + @effects[PBEffects::Flinch] = false + @effects[PBEffects::FocusPunch] = false + @effects[PBEffects::FollowMe] = 0 + @effects[PBEffects::Foresight] = false + @effects[PBEffects::FuryCutter] = 0 + @effects[PBEffects::GemConsumed] = nil + @effects[PBEffects::Grudge] = false + @effects[PBEffects::HelpingHand] = false + @effects[PBEffects::HyperBeam] = 0 + @effects[PBEffects::Illusion] = nil + if hasActiveAbility?(:ILLUSION) + idxLastParty = @battle.pbLastInTeam(@index) + if idxLastParty >= 0 && idxLastParty != @pokemonIndex + @effects[PBEffects::Illusion] = @battle.pbParty(@index)[idxLastParty] + end + end + @effects[PBEffects::Imprison] = false + @effects[PBEffects::Instruct] = false + @effects[PBEffects::Instructed] = false + @effects[PBEffects::KingsShield] = false + @battle.eachBattler do |b| # Other battlers lose their lock-on against self + next if b.effects[PBEffects::LockOn]==0 + next if b.effects[PBEffects::LockOnPos]!=@index + b.effects[PBEffects::LockOn] = 0 + b.effects[PBEffects::LockOnPos] = -1 + end + @effects[PBEffects::MagicBounce] = false + @effects[PBEffects::MagicCoat] = false + @effects[PBEffects::MeanLook] = -1 + @battle.eachBattler do |b| # Other battlers no longer blocked by self + b.effects[PBEffects::MeanLook] = -1 if b.effects[PBEffects::MeanLook]==@index + end + @effects[PBEffects::MeFirst] = false + @effects[PBEffects::Metronome] = 0 + @effects[PBEffects::MicleBerry] = false + @effects[PBEffects::Minimize] = false + @effects[PBEffects::MiracleEye] = false + @effects[PBEffects::MirrorCoat] = -1 + @effects[PBEffects::MirrorCoatTarget] = -1 + @effects[PBEffects::MoveNext] = false + @effects[PBEffects::MudSport] = false + @effects[PBEffects::Nightmare] = false + @effects[PBEffects::Outrage] = 0 + @effects[PBEffects::ParentalBond] = 0 + @effects[PBEffects::PickupItem] = nil + @effects[PBEffects::PickupUse] = 0 + @effects[PBEffects::Pinch] = false + @effects[PBEffects::Powder] = false + @effects[PBEffects::Prankster] = false + @effects[PBEffects::PriorityAbility] = false + @effects[PBEffects::PriorityItem] = false + @effects[PBEffects::Protect] = false + @effects[PBEffects::ProtectRate] = 1 + @effects[PBEffects::Pursuit] = false + @effects[PBEffects::Quash] = 0 + @effects[PBEffects::Rage] = false + @effects[PBEffects::RagePowder] = false + @effects[PBEffects::Rollout] = 0 + @effects[PBEffects::Roost] = false + @effects[PBEffects::SkyDrop] = -1 + @battle.eachBattler do |b| # Other battlers no longer Sky Dropped by self + b.effects[PBEffects::SkyDrop] = -1 if b.effects[PBEffects::SkyDrop]==@index + end + @effects[PBEffects::SlowStart] = 0 + @effects[PBEffects::SmackDown] = false + @effects[PBEffects::Snatch] = 0 + @effects[PBEffects::SpikyShield] = false + @effects[PBEffects::Spotlight] = 0 + @effects[PBEffects::Stockpile] = 0 + @effects[PBEffects::StockpileDef] = 0 + @effects[PBEffects::StockpileSpDef] = 0 + @effects[PBEffects::Taunt] = 0 + @effects[PBEffects::ThroatChop] = 0 + @effects[PBEffects::Torment] = false + @effects[PBEffects::Toxic] = 0 + @effects[PBEffects::Transform] = false + @effects[PBEffects::TransformSpecies] = 0 + @effects[PBEffects::Trapping] = 0 + @effects[PBEffects::TrappingMove] = nil + @effects[PBEffects::TrappingUser] = -1 + @battle.eachBattler do |b| # Other battlers no longer trapped by self + next if b.effects[PBEffects::TrappingUser]!=@index + b.effects[PBEffects::Trapping] = 0 + b.effects[PBEffects::TrappingUser] = -1 + end + @effects[PBEffects::Truant] = false + @effects[PBEffects::TwoTurnAttack] = nil + @effects[PBEffects::Type3] = nil + @effects[PBEffects::Unburden] = false + @effects[PBEffects::Uproar] = 0 + @effects[PBEffects::WaterSport] = false + @effects[PBEffects::WeightChange] = 0 + @effects[PBEffects::Yawn] = 0 + end + + #============================================================================= + # Refreshing a battler's properties + #============================================================================= + def pbUpdate(fullChange=false) + return if !@pokemon + @pokemon.calc_stats + @level = @pokemon.level + @hp = @pokemon.hp + @totalhp = @pokemon.totalhp + if !@effects[PBEffects::Transform] + @attack = @pokemon.attack + @defense = @pokemon.defense + @spatk = @pokemon.spatk + @spdef = @pokemon.spdef + @speed = @pokemon.speed + if fullChange + @type1 = @pokemon.type1 + @type2 = @pokemon.type2 + @ability_id = @pokemon.ability_id + end + end + end + + # Used to erase the battler of a Pokémon that has been caught. + def pbReset + @pokemon = nil + @pokemonIndex = -1 + @hp = 0 + pbInitEffects(false) + @participants = [] + # Reset status + @status = :NONE + @statusCount = 0 + # Reset choice + @battle.pbClearChoice(@index) + end + + # Update which Pokémon will gain Exp if this battler is defeated. + def pbUpdateParticipants + return if fainted? || !@battle.opposes?(@index) + eachOpposing do |b| + @participants.push(b.pokemonIndex) if !@participants.include?(b.pokemonIndex) + end + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/003_Battler_ChangeSelf.rb b/Data/Scripts/011_Battle/001_Battler/003_Battler_ChangeSelf.rb new file mode 100644 index 000000000..ba8f420f2 --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/003_Battler_ChangeSelf.rb @@ -0,0 +1,312 @@ +class PokeBattle_Battler + #============================================================================= + # Change HP + #============================================================================= + def pbReduceHP(amt,anim=true,registerDamage=true,anyAnim=true) + amt = amt.round + amt = @hp if amt>@hp + amt = 1 if amt<1 && !fainted? + oldHP = @hp + self.hp -= amt + PBDebug.log("[HP change] #{pbThis} lost #{amt} HP (#{oldHP}=>#{@hp})") if amt>0 + raise _INTL("HP less than 0") if @hp<0 + raise _INTL("HP greater than total HP") if @hp>@totalhp + @battle.scene.pbHPChanged(self,oldHP,anim) if anyAnim && amt>0 + @tookDamage = true if amt>0 && registerDamage + return amt + end + + def pbRecoverHP(amt,anim=true,anyAnim=true) + amt = amt.round + amt = @totalhp-@hp if amt>@totalhp-@hp + amt = 1 if amt<1 && @hp<@totalhp + oldHP = @hp + self.hp += amt + PBDebug.log("[HP change] #{pbThis} gained #{amt} HP (#{oldHP}=>#{@hp})") if amt>0 + raise _INTL("HP less than 0") if @hp<0 + raise _INTL("HP greater than total HP") if @hp>@totalhp + @battle.scene.pbHPChanged(self,oldHP,anim) if anyAnim && amt>0 + return amt + end + + def pbRecoverHPFromDrain(amt,target,msg=nil) + if target.hasActiveAbility?(:LIQUIDOOZE, true) + @battle.pbShowAbilitySplash(target) + pbReduceHP(amt) + @battle.pbDisplay(_INTL("{1} sucked up the liquid ooze!",pbThis)) + @battle.pbHideAbilitySplash(target) + pbItemHPHealCheck + else + msg = _INTL("{1} had its energy drained!",target.pbThis) if nil_or_empty?(msg) + @battle.pbDisplay(msg) + if canHeal? + amt = (amt*1.3).floor if hasActiveItem?(:BIGROOT) + pbRecoverHP(amt) + end + end + end + + def pbFaint(showMessage=true) + if !fainted? + PBDebug.log("!!!***Can't faint with HP greater than 0") + return + end + return if @fainted # Has already fainted properly + @battle.pbDisplayBrief(_INTL("{1} fainted!",pbThis)) if showMessage + updateSpirits() + PBDebug.log("[Pokémon fainted] #{pbThis} (#{@index})") if !showMessage + @battle.scene.pbFaintBattler(self) + pbInitEffects(false) + # Reset status + self.status = :NONE + self.statusCount = 0 + # Lose happiness + if @pokemon && @battle.internalBattle + badLoss = false + @battle.eachOtherSideBattler(@index) do |b| + badLoss = true if b.level>=self.level+30 + end + @pokemon.changeHappiness((badLoss) ? "faintbad" : "faint") + end + # Reset form + @battle.peer.pbOnLeavingBattle(@battle,@pokemon,@battle.usedInBattle[idxOwnSide][@index/2]) + @pokemon.makeUnmega if mega? + @pokemon.makeUnprimal if primal? + # Do other things + @battle.pbClearChoice(@index) # Reset choice + pbOwnSide.effects[PBEffects::LastRoundFainted] = @battle.turnCount + # Check other battlers' abilities that trigger upon a battler fainting + pbAbilitiesOnFainting + # Check for end of primordial weather + @battle.pbEndPrimordialWeather + end + + def updateSpirits() + if $PokemonBag.pbQuantity(:ODDKEYSTONE)>=1 && @pokemon.hasType?(:GHOST) + nbSpirits = pbGet(VAR_ODDKEYSTONE_NB) + if nbSpirits < 108 + pbSet(VAR_ODDKEYSTONE_NB, nbSpirits+1) + end + end + end + + #============================================================================= + # Move PP + #============================================================================= + def pbSetPP(move,pp) + move.pp = pp + # No need to care about @effects[PBEffects::Mimic], since Mimic can't copy + # Mimic + if move.realMove && move.id==move.realMove.id && !@effects[PBEffects::Transform] + move.realMove.pp = pp + end + end + + def pbReducePP(move) + return true if usingMultiTurnAttack? + return true if move.pp<0 # Don't reduce PP for special calls of moves + return true if move.total_pp<=0 # Infinite PP, can always be used + return false if move.pp==0 # Ran out of PP, couldn't reduce + pbSetPP(move,move.pp-1) if move.pp>0 + return true + end + + def pbReducePPOther(move) + pbSetPP(move,move.pp-1) if move.pp>0 + end + + #============================================================================= + # Change type + #============================================================================= + def pbChangeTypes(newType) + if newType.is_a?(PokeBattle_Battler) + newTypes = newType.pbTypes + newTypes.push(:NORMAL) if newTypes.length == 0 + newType3 = newType.effects[PBEffects::Type3] + newType3 = nil if newTypes.include?(newType3) + @type1 = newTypes[0] + @type2 = (newTypes.length == 1) ? newTypes[0] : newTypes[1] + @effects[PBEffects::Type3] = newType3 + else + newType = GameData::Type.get(newType).id + @type1 = newType + @type2 = newType + @effects[PBEffects::Type3] = nil + end + @effects[PBEffects::BurnUp] = false + @effects[PBEffects::Roost] = false + end + + #============================================================================= + # Forms + #============================================================================= + def pbChangeForm(newForm,msg) + return if fainted? || @effects[PBEffects::Transform] || @form==newForm + oldForm = @form + oldDmg = @totalhp-@hp + self.form = newForm + pbUpdate(true) + @hp = @totalhp-oldDmg + @effects[PBEffects::WeightChange] = 0 if Settings::MECHANICS_GENERATION >= 6 + @battle.scene.pbChangePokemon(self,@pokemon) + @battle.scene.pbRefreshOne(@index) + @battle.pbDisplay(msg) if msg && msg!="" + PBDebug.log("[Form changed] #{pbThis} changed from form #{oldForm} to form #{newForm}") + @battle.pbSetSeen(self) + end + + def pbCheckFormOnStatusChange + return if fainted? || @effects[PBEffects::Transform] + # Shaymin - reverts if frozen + if isSpecies?(:SHAYMIN) && frozen? + pbChangeForm(0,_INTL("{1} transformed!",pbThis)) + end + end + + def pbCheckFormOnMovesetChange + return if fainted? || @effects[PBEffects::Transform] + # Keldeo - knowing Secret Sword + if isSpecies?(:KELDEO) + newForm = 0 + newForm = 1 if pbHasMove?(:SECRETSWORD) + pbChangeForm(newForm,_INTL("{1} transformed!",pbThis)) + end + end + + def pbCheckFormOnWeatherChange + return if fainted? || @effects[PBEffects::Transform] + # Castform - Forecast + if isSpecies?(:CASTFORM) + if hasActiveAbility?(:FORECAST) + newForm = 0 + case @battle.pbWeather + when :Sun, :HarshSun then newForm = 1 + when :Rain, :HeavyRain then newForm = 2 + when :Hail then newForm = 3 + end + if @form!=newForm + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(newForm,_INTL("{1} transformed!",pbThis)) + end + else + pbChangeForm(0,_INTL("{1} transformed!",pbThis)) + end + end + # Cherrim - Flower Gift + if isSpecies?(:CHERRIM) + if hasActiveAbility?(:FLOWERGIFT) + newForm = 0 + newForm = 1 if [:Sun, :HarshSun].include?(@battle.pbWeather) + if @form!=newForm + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(newForm,_INTL("{1} transformed!",pbThis)) + end + else + pbChangeForm(0,_INTL("{1} transformed!",pbThis)) + end + end + end + + # Checks the Pokémon's form and updates it if necessary. Used for when a + # Pokémon enters battle (endOfRound=false) and at the end of each round + # (endOfRound=true). + def pbCheckForm(endOfRound=false) + return if fainted? || @effects[PBEffects::Transform] + # Form changes upon entering battle and when the weather changes + pbCheckFormOnWeatherChange if !endOfRound + # Darmanitan - Zen Mode + if isSpecies?(:DARMANITAN) && self.ability == :ZENMODE + if @hp<=@totalhp/2 + if @form!=1 + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(1,_INTL("{1} triggered!",abilityName)) + end + elsif @form!=0 + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(0,_INTL("{1} triggered!",abilityName)) + end + end + # Minior - Shields Down + if isSpecies?(:MINIOR) && self.ability == :SHIELDSDOWN + if @hp>@totalhp/2 # Turn into Meteor form + newForm = (@form>=7) ? @form-7 : @form + if @form!=newForm + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(newForm,_INTL("{1} deactivated!",abilityName)) + elsif !endOfRound + @battle.pbDisplay(_INTL("{1} deactivated!",abilityName)) + end + elsif @form<7 # Turn into Core form + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(@form+7,_INTL("{1} activated!",abilityName)) + end + end + # Wishiwashi - Schooling + if isSpecies?(:WISHIWASHI) && self.ability == :SCHOOLING + if @level>=20 && @hp>@totalhp/4 + if @form!=1 + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(1,_INTL("{1} formed a school!",pbThis)) + end + elsif @form!=0 + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(0,_INTL("{1} stopped schooling!",pbThis)) + end + end + # Zygarde - Power Construct + if isSpecies?(:ZYGARDE) && self.ability == :POWERCONSTRUCT && endOfRound + if @hp<=@totalhp/2 && @form<2 # Turn into Complete Forme + newForm = @form+2 + @battle.pbDisplay(_INTL("You sense the presence of many!")) + @battle.pbShowAbilitySplash(self,true) + @battle.pbHideAbilitySplash(self) + pbChangeForm(newForm,_INTL("{1} transformed into its Complete Forme!",pbThis)) + end + end + end + + def pbTransform(target) + if target.is_a?(Integer) + @battle.pbDisplay(_INTL("But it failed...")) + return + end + oldAbil = @ability_id + @effects[PBEffects::Transform] = true + @effects[PBEffects::TransformSpecies] = target.species + pbChangeTypes(target) + self.ability = target.ability + self.ability2 = target.ability2 if target.ability2 + @attack = target.attack + @defense = target.defense + @spatk = target.spatk + @spdef = target.spdef + @speed = target.speed + GameData::Stat.each_battle { |s| @stages[s.id] = target.stages[s.id] } + if Settings::NEW_CRITICAL_HIT_RATE_MECHANICS + @effects[PBEffects::FocusEnergy] = target.effects[PBEffects::FocusEnergy] + @effects[PBEffects::LaserFocus] = target.effects[PBEffects::LaserFocus] + end + @moves.clear + target.moves.each_with_index do |m,i| + @moves[i] = PokeBattle_Move.from_pokemon_move(@battle, Pokemon::Move.new(m.id)) + @moves[i].pp = 5 + @moves[i].total_pp = 5 + end + @effects[PBEffects::Disable] = 0 + @effects[PBEffects::DisableMove] = nil + @effects[PBEffects::WeightChange] = target.effects[PBEffects::WeightChange] + @battle.scene.pbRefreshOne(@index) + @battle.pbDisplay(_INTL("{1} transformed into {2}!",pbThis,target.pbThis(true))) + pbOnAbilityChanged(oldAbil) + end + + def pbHyperMode; end +end diff --git a/Data/Scripts/011_Battle/001_Battler/004_Battler_Statuses.rb b/Data/Scripts/011_Battle/001_Battler/004_Battler_Statuses.rb new file mode 100644 index 000000000..733192ec3 --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/004_Battler_Statuses.rb @@ -0,0 +1,576 @@ +class PokeBattle_Battler + #============================================================================= + # Generalised checks for whether a status problem can be inflicted + #============================================================================= + # NOTE: Not all "does it have this status?" checks use this method. If the + # check is leading up to curing self of that status condition, then it + # will look at the value of @status directly instead - if it is that + # status condition then it is curable. This method only checks for + # "counts as having that status", which includes Comatose which can't be + # cured. + def pbHasStatus?(checkStatus) + if BattleHandlers.triggerStatusCheckAbilityNonIgnorable(self.ability,self,checkStatus) + return true + end + return @status==checkStatus + end + + def pbHasAnyStatus? + if BattleHandlers.triggerStatusCheckAbilityNonIgnorable(self.ability,self,nil) + return true + end + return @status != :NONE + end + + def pbCanInflictStatus?(newStatus,user,showMessages,move=nil,ignoreStatus=false) + return false if fainted? + selfInflicted = (user && user.index==@index) + # Already have that status problem + if self.status==newStatus && !ignoreStatus + if showMessages + msg = "" + case self.status + when :SLEEP then msg = _INTL("{1} is already asleep!", pbThis) + when :POISON then msg = _INTL("{1} is already poisoned!", pbThis) + when :BURN then msg = _INTL("{1} already has a burn!", pbThis) + when :PARALYSIS then msg = _INTL("{1} is already paralyzed!", pbThis) + when :FROZEN then msg = _INTL("{1} is already frozen solid!", pbThis) + end + @battle.pbDisplay(msg) + end + return false + end + # Trying to replace a status problem with another one + if self.status != :NONE && !ignoreStatus && !selfInflicted + @battle.pbDisplay(_INTL("It doesn't affect {1}...",pbThis(true))) if showMessages + return false + end + # Trying to inflict a status problem on a Pokémon behind a substitute + if @effects[PBEffects::Substitute]>0 && !(move && move.ignoresSubstitute?(user)) && + !selfInflicted + @battle.pbDisplay(_INTL("It doesn't affect {1}...",pbThis(true))) if showMessages + return false + end + # Weather immunity + if newStatus == :FROZEN && [:Sun, :HarshSun].include?(@battle.pbWeather) + @battle.pbDisplay(_INTL("It doesn't affect {1}...",pbThis(true))) if showMessages + return false + end + # Terrains immunity + if affectedByTerrain? + case @battle.field.terrain + when :Electric + if newStatus == :SLEEP + @battle.pbDisplay(_INTL("{1} surrounds itself with electrified terrain!", + pbThis(true))) if showMessages + return false + end + when :Misty + @battle.pbDisplay(_INTL("{1} surrounds itself with misty terrain!",pbThis(true))) if showMessages + return false + end + end + # Uproar immunity + if newStatus == :SLEEP && !(hasActiveAbility?(:SOUNDPROOF) && !@battle.moldBreaker) + @battle.eachBattler do |b| + next if b.effects[PBEffects::Uproar]==0 + @battle.pbDisplay(_INTL("But the uproar kept {1} awake!",pbThis(true))) if showMessages + return false + end + end + # Type immunities + hasImmuneType = false + case newStatus + when :SLEEP + # No type is immune to sleep + when :POISON + if !(user && user.hasActiveAbility?(:CORROSION)) + hasImmuneType |= pbHasType?(:POISON) + hasImmuneType |= pbHasType?(:STEEL) + end + when :BURN + hasImmuneType |= pbHasType?(:FIRE) + when :PARALYSIS + hasImmuneType |= pbHasType?(:ELECTRIC) && Settings::MORE_TYPE_EFFECTS + when :FROZEN + hasImmuneType |= pbHasType?(:ICE) + end + if hasImmuneType + @battle.pbDisplay(_INTL("It doesn't affect {1}...",pbThis(true))) if showMessages + return false + end + # Ability immunity + immuneByAbility = false; immAlly = nil + if BattleHandlers.triggerStatusImmunityAbilityNonIgnorable(self.ability,self,newStatus) + immuneByAbility = true + elsif selfInflicted || !@battle.moldBreaker + if abilityActive? && BattleHandlers.triggerStatusImmunityAbility(self.ability,self,newStatus) + immuneByAbility = true + else + eachAlly do |b| + next if !b.abilityActive? + next if !BattleHandlers.triggerStatusImmunityAllyAbility(b.ability,self,newStatus) + immuneByAbility = true + immAlly = b + break + end + end + end + if immuneByAbility + if showMessages + @battle.pbShowAbilitySplash(immAlly || self) + msg = "" + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + case newStatus + when :SLEEP then msg = _INTL("{1} stays awake!", pbThis) + when :POISON then msg = _INTL("{1} cannot be poisoned!", pbThis) + when :BURN then msg = _INTL("{1} cannot be burned!", pbThis) + when :PARALYSIS then msg = _INTL("{1} cannot be paralyzed!", pbThis) + when :FROZEN then msg = _INTL("{1} cannot be frozen solid!", pbThis) + end + elsif immAlly + case newStatus + when :SLEEP + msg = _INTL("{1} stays awake because of {2}'s {3}!", + pbThis,immAlly.pbThis(true),immAlly.abilityName) + when :POISON + msg = _INTL("{1} cannot be poisoned because of {2}'s {3}!", + pbThis,immAlly.pbThis(true),immAlly.abilityName) + when :BURN + msg = _INTL("{1} cannot be burned because of {2}'s {3}!", + pbThis,immAlly.pbThis(true),immAlly.abilityName) + when :PARALYSIS + msg = _INTL("{1} cannot be paralyzed because of {2}'s {3}!", + pbThis,immAlly.pbThis(true),immAlly.abilityName) + when :FROZEN + msg = _INTL("{1} cannot be frozen solid because of {2}'s {3}!", + pbThis,immAlly.pbThis(true),immAlly.abilityName) + end + else + case newStatus + when :SLEEP then msg = _INTL("{1} stays awake because of its {2}!", pbThis, abilityName) + when :POISON then msg = _INTL("{1}'s {2} prevents poisoning!", pbThis, abilityName) + when :BURN then msg = _INTL("{1}'s {2} prevents burns!", pbThis, abilityName) + when :PARALYSIS then msg = _INTL("{1}'s {2} prevents paralysis!", pbThis, abilityName) + when :FROZEN then msg = _INTL("{1}'s {2} prevents freezing!", pbThis, abilityName) + end + end + @battle.pbDisplay(msg) + @battle.pbHideAbilitySplash(immAlly || self) + end + return false + end + # Safeguard immunity + if pbOwnSide.effects[PBEffects::Safeguard]>0 && !selfInflicted && move && + !(user && user.hasActiveAbility?(:INFILTRATOR)) + @battle.pbDisplay(_INTL("{1}'s team is protected by Safeguard!",pbThis)) if showMessages + return false + end + return true + end + + def pbCanSynchronizeStatus?(newStatus,target) + return false if fainted? + # Trying to replace a status problem with another one + return false if self.status != :NONE + # Terrain immunity + return false if @battle.field.terrain == :Misty && affectedByTerrain? + # Type immunities + hasImmuneType = false + case newStatus + when :POISON + # NOTE: target will have Synchronize, so it can't have Corrosion. + if !(target && target.hasActiveAbility?(:CORROSION)) + hasImmuneType |= pbHasType?(:POISON) + hasImmuneType |= pbHasType?(:STEEL) + end + when :BURN + hasImmuneType |= pbHasType?(:FIRE) + when :PARALYSIS + hasImmuneType |= pbHasType?(:ELECTRIC) && Settings::MORE_TYPE_EFFECTS + end + return false if hasImmuneType + # Ability immunity + if BattleHandlers.triggerStatusImmunityAbilityNonIgnorable(self.ability,self,newStatus) + return false + end + if abilityActive? && BattleHandlers.triggerStatusImmunityAbility(self.ability,self,newStatus) + return false + end + eachAlly do |b| + next if !b.abilityActive? + next if !BattleHandlers.triggerStatusImmunityAllyAbility(b.ability,self,newStatus) + return false + end + # Safeguard immunity + if pbOwnSide.effects[PBEffects::Safeguard]>0 && + !(user && user.hasActiveAbility?(:INFILTRATOR)) + return false + end + return true + end + + #============================================================================= + # Generalised infliction of status problem + #============================================================================= + def pbInflictStatus(newStatus,newStatusCount=0,msg=nil,user=nil) + # Inflict the new status + self.status = newStatus + self.statusCount = newStatusCount + @effects[PBEffects::Toxic] = 0 + # Show animation + if newStatus == :POISON && newStatusCount > 0 + @battle.pbCommonAnimation("Toxic", self) + else + anim_name = GameData::Status.get(newStatus).animation + @battle.pbCommonAnimation(anim_name, self) if anim_name + end + # Show message + if msg && !msg.empty? + @battle.pbDisplay(msg) + else + case newStatus + when :SLEEP + @battle.pbDisplay(_INTL("{1} fell asleep!", pbThis)) + when :POISON + if newStatusCount>0 + @battle.pbDisplay(_INTL("{1} was badly poisoned!", pbThis)) + else + @battle.pbDisplay(_INTL("{1} was poisoned!", pbThis)) + end + when :BURN + @battle.pbDisplay(_INTL("{1} was burned!", pbThis)) + when :PARALYSIS + @battle.pbDisplay(_INTL("{1} is paralyzed! It may be unable to move!", pbThis)) + when :FROZEN + @battle.pbDisplay(_INTL("{1} was frozen solid!", pbThis)) + end + end + PBDebug.log("[Status change] #{pbThis}'s sleep count is #{newStatusCount}") if newStatus == :SLEEP + # Form change check + pbCheckFormOnStatusChange + # Synchronize + if abilityActive? + BattleHandlers.triggerAbilityOnStatusInflicted(self.ability,self,user,newStatus) + end + # Status cures + pbItemStatusCureCheck + pbAbilityStatusCureCheck + # Petal Dance/Outrage/Thrash get cancelled immediately by falling asleep + # NOTE: I don't know why this applies only to Outrage and only to falling + # asleep (i.e. it doesn't cancel Rollout/Uproar/other multi-turn + # moves, and it doesn't cancel any moves if self becomes frozen/ + # disabled/anything else). This behaviour was tested in Gen 5. + if @status == :SLEEP && @effects[PBEffects::Outrage] > 0 + @effects[PBEffects::Outrage] = 0 + @currentMove = nil + end + end + + #============================================================================= + # Sleep + #============================================================================= + def asleep? + return pbHasStatus?(:SLEEP) + end + + def pbCanSleep?(user, showMessages, move = nil, ignoreStatus = false) + return pbCanInflictStatus?(:SLEEP, user, showMessages, move, ignoreStatus) + end + + def pbCanSleepYawn? + return false if self.status != :NONE + if affectedByTerrain? + return false if [:Electric, :Misty].include?(@battle.field.terrain) + end + if !hasActiveAbility?(:SOUNDPROOF) + @battle.eachBattler do |b| + return false if b.effects[PBEffects::Uproar]>0 + end + end + if BattleHandlers.triggerStatusImmunityAbilityNonIgnorable(self.ability, self, :SLEEP) + return false + end + # NOTE: Bulbapedia claims that Flower Veil shouldn't prevent sleep due to + # drowsiness, but I disagree because that makes no sense. Also, the + # comparable Sweet Veil does prevent sleep due to drowsiness. + if abilityActive? && BattleHandlers.triggerStatusImmunityAbility(self.ability, self, :SLEEP) + return false + end + eachAlly do |b| + next if !b.abilityActive? + next if !BattleHandlers.triggerStatusImmunityAllyAbility(b.ability, self, :SLEEP) + return false + end + # NOTE: Bulbapedia claims that Safeguard shouldn't prevent sleep due to + # drowsiness. I disagree with this too. Compare with the other sided + # effects Misty/Electric Terrain, which do prevent it. + return false if pbOwnSide.effects[PBEffects::Safeguard]>0 + return true + end + + def pbSleep(msg = nil) + pbInflictStatus(:SLEEP, pbSleepDuration, msg) + end + + def pbSleepSelf(msg = nil, duration = -1) + pbInflictStatus(:SLEEP, pbSleepDuration(duration), msg) + end + + def pbSleepDuration(duration = -1) + duration = 2 + @battle.pbRandom(3) if duration <= 0 + duration = (duration / 2).floor if hasActiveAbility?(:EARLYBIRD) + return duration + end + + #============================================================================= + # Poison + #============================================================================= + def poisoned? + return pbHasStatus?(:POISON) + end + + def pbCanPoison?(user, showMessages, move = nil) + return pbCanInflictStatus?(:POISON, user, showMessages, move) + end + + def pbCanPoisonSynchronize?(target) + return pbCanSynchronizeStatus?(:POISON, target) + end + + def pbPoison(user = nil, msg = nil, toxic = false) + pbInflictStatus(:POISON, (toxic) ? 1 : 0, msg, user) + end + + #============================================================================= + # Burn + #============================================================================= + def burned? + return pbHasStatus?(:BURN) + end + + def pbCanBurn?(user, showMessages, move = nil) + return pbCanInflictStatus?(:BURN, user, showMessages, move) + end + + def pbCanBurnSynchronize?(target) + return pbCanSynchronizeStatus?(:BURN, target) + end + + def pbBurn(user = nil, msg = nil) + pbInflictStatus(:BURN, 0, msg, user) + end + + #============================================================================= + # Paralyze + #============================================================================= + def paralyzed? + return pbHasStatus?(:PARALYSIS) + end + + def pbCanParalyze?(user, showMessages, move = nil) + return pbCanInflictStatus?(:PARALYSIS, user, showMessages, move) + end + + def pbCanParalyzeSynchronize?(target) + return pbCanSynchronizeStatus?(:PARALYSIS, target) + end + + def pbParalyze(user = nil, msg = nil) + pbInflictStatus(:PARALYSIS, 0, msg, user) + end + + #============================================================================= + # Freeze + #============================================================================= + def frozen? + return pbHasStatus?(:FROZEN) + end + + def pbCanFreeze?(user, showMessages, move = nil) + return pbCanInflictStatus?(:FROZEN, user, showMessages, move) + end + + def pbFreeze(msg = nil) + pbInflictStatus(:FROZEN, 0, msg) + end + + #============================================================================= + # Generalised status displays + #============================================================================= + def pbContinueStatus + if self.status == :POISON && @statusCount > 0 + @battle.pbCommonAnimation("Toxic", self) + else + anim_name = GameData::Status.get(self.status).animation + @battle.pbCommonAnimation(anim_name, self) if anim_name + end + yield if block_given? + case self.status + when :SLEEP + @battle.pbDisplay(_INTL("{1} is fast asleep.", pbThis)) + when :POISON + @battle.pbDisplay(_INTL("{1} was hurt by poison!", pbThis)) + when :BURN + @battle.pbDisplay(_INTL("{1} was hurt by its burn!", pbThis)) + when :PARALYSIS + @battle.pbDisplay(_INTL("{1} is paralyzed! It can't move!", pbThis)) + when :FROZEN + @battle.pbDisplay(_INTL("{1} is frozen solid!", pbThis)) + end + PBDebug.log("[Status continues] #{pbThis}'s sleep count is #{@statusCount}") if self.status == :SLEEP + end + + def pbCureStatus(showMessages=true) + oldStatus = status + self.status = :NONE + if showMessages + case oldStatus + when :SLEEP then @battle.pbDisplay(_INTL("{1} woke up!", pbThis)) + when :POISON then @battle.pbDisplay(_INTL("{1} was cured of its poisoning.", pbThis)) + when :BURN then @battle.pbDisplay(_INTL("{1}'s burn was healed.", pbThis)) + when :PARALYSIS then @battle.pbDisplay(_INTL("{1} was cured of paralysis.", pbThis)) + when :FROZEN then @battle.pbDisplay(_INTL("{1} thawed out!", pbThis)) + end + end + PBDebug.log("[Status change] #{pbThis}'s status was cured") if !showMessages + end + + #============================================================================= + # Confusion + #============================================================================= + def pbCanConfuse?(user=nil,showMessages=true,move=nil,selfInflicted=false) + return false if fainted? + if @effects[PBEffects::Confusion]>0 + @battle.pbDisplay(_INTL("{1} is already confused.",pbThis)) if showMessages + return false + end + if @effects[PBEffects::Substitute]>0 && !(move && move.ignoresSubstitute?(user)) && + !selfInflicted + @battle.pbDisplay(_INTL("But it failed!")) if showMessages + return false + end + # Terrains immunity + if affectedByTerrain? && @battle.field.terrain == :Misty + @battle.pbDisplay(_INTL("{1} surrounds itself with misty terrain!",pbThis(true))) if showMessages + return false + end + if selfInflicted || !@battle.moldBreaker + if hasActiveAbility?(:OWNTEMPO) + if showMessages + @battle.pbShowAbilitySplash(self) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} doesn't become confused!",pbThis)) + else + @battle.pbDisplay(_INTL("{1}'s {2} prevents confusion!",pbThis,abilityName)) + end + @battle.pbHideAbilitySplash(self) + end + return false + end + end + if pbOwnSide.effects[PBEffects::Safeguard]>0 && !selfInflicted && + !(user && user.hasActiveAbility?(:INFILTRATOR)) + @battle.pbDisplay(_INTL("{1}'s team is protected by Safeguard!",pbThis)) if showMessages + return false + end + return true + end + + def pbCanConfuseSelf?(showMessages) + return pbCanConfuse?(nil,showMessages,nil,true) + end + + def pbConfuse(msg=nil) + @effects[PBEffects::Confusion] = pbConfusionDuration + @battle.pbCommonAnimation("Confusion",self) + msg = _INTL("{1} became confused!",pbThis) if nil_or_empty?(msg) + @battle.pbDisplay(msg) + PBDebug.log("[Lingering effect] #{pbThis}'s confusion count is #{@effects[PBEffects::Confusion]}") + # Confusion cures + pbItemStatusCureCheck + pbAbilityStatusCureCheck + end + + def pbConfusionDuration(duration=-1) + duration = 2+@battle.pbRandom(4) if duration<=0 + return duration + end + + def pbCureConfusion + @effects[PBEffects::Confusion] = 0 + end + + #============================================================================= + # Attraction + #============================================================================= + def pbCanAttract?(user,showMessages=true) + return false if fainted? + return false if !user || user.fainted? + if @effects[PBEffects::Attract]>=0 + @battle.pbDisplay(_INTL("{1} is unaffected!",pbThis)) if showMessages + return false + end + agender = user.gender + ogender = gender + if agender==2 || ogender==2 || agender==ogender + @battle.pbDisplay(_INTL("{1} is unaffected!",pbThis)) if showMessages + return false + end + if !@battle.moldBreaker + if hasActiveAbility?([:AROMAVEIL,:OBLIVIOUS]) + if showMessages + @battle.pbShowAbilitySplash(self) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!",pbThis)) + else + @battle.pbDisplay(_INTL("{1}'s {2} prevents romance!",pbThis,abilityName)) + end + @battle.pbHideAbilitySplash(self) + end + return false + else + eachAlly do |b| + next if !b.hasActiveAbility?(:AROMAVEIL) + if showMessages + @battle.pbShowAbilitySplash(self) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!",pbThis)) + else + @battle.pbDisplay(_INTL("{1}'s {2} prevents romance!",b.pbThis,b.abilityName)) + end + @battle.pbHideAbilitySplash(self) + end + return true + end + end + end + return true + end + + def pbAttract(user,msg=nil) + @effects[PBEffects::Attract] = user.index + @battle.pbCommonAnimation("Attract",self) + msg = _INTL("{1} fell in love!",pbThis) if nil_or_empty?(msg) + @battle.pbDisplay(msg) + # Destiny Knot + if hasActiveItem?(:DESTINYKNOT) && user.pbCanAttract?(self,false) + user.pbAttract(self,_INTL("{1} fell in love from the {2}!",user.pbThis(true),itemName)) + end + # Attraction cures + pbItemStatusCureCheck + pbAbilityStatusCureCheck + end + + def pbCureAttract + @effects[PBEffects::Attract] = -1 + end + + #============================================================================= + # Flinching + #============================================================================= + def pbFlinch(_user=nil) + return if hasActiveAbility?(:INNERFOCUS) && !@battle.moldBreaker + @effects[PBEffects::Flinch] = true + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/005_Battler_StatStages.rb b/Data/Scripts/011_Battle/001_Battler/005_Battler_StatStages.rb new file mode 100644 index 000000000..978728c5f --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/005_Battler_StatStages.rb @@ -0,0 +1,308 @@ +class PokeBattle_Battler + #============================================================================= + # Increase stat stages + #============================================================================= + def statStageAtMax?(stat) + return @stages[stat]>=6 + end + + def pbCanRaiseStatStage?(stat,user=nil,move=nil,showFailMsg=false,ignoreContrary=false) + return false if fainted? + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary && !@battle.moldBreaker + return pbCanLowerStatStage?(stat,user,move,showFailMsg,true) + end + # Check the stat stage + if statStageAtMax?(stat) + @battle.pbDisplay(_INTL("{1}'s {2} won't go any higher!", + pbThis, GameData::Stat.get(stat).name)) if showFailMsg + return false + end + return true + end + + def pbRaiseStatStageBasic(stat,increment,ignoreContrary=false) + if !@battle.moldBreaker + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary + return pbLowerStatStageBasic(stat,increment,true) + end + # Simple + increment *= 2 if hasActiveAbility?(:SIMPLE) + end + # Change the stat stage + increment = [increment,6-@stages[stat]].min + if increment>0 + stat_name = GameData::Stat.get(stat).name + new = @stages[stat]+increment + PBDebug.log("[Stat change] #{pbThis}'s #{stat_name}: #{@stages[stat]} -> #{new} (+#{increment})") + @stages[stat] += increment + end + return increment + end + + def pbRaiseStatStage(stat,increment,user,showAnim=true,ignoreContrary=false) + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary && !@battle.moldBreaker + return pbLowerStatStage(stat,increment,user,showAnim,true) + end + # Perform the stat stage change + increment = pbRaiseStatStageBasic(stat,increment,ignoreContrary) + return false if increment<=0 + # Stat up animation and message + @battle.pbCommonAnimation("StatUp",self) if showAnim + arrStatTexts = [ + _INTL("{1}'s {2} rose!",pbThis,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} rose sharply!",pbThis,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} rose drastically!",pbThis,GameData::Stat.get(stat).name)] + @battle.pbDisplay(arrStatTexts[[increment-1,2].min]) + # Trigger abilities upon stat gain + if abilityActive? + BattleHandlers.triggerAbilityOnStatGain(self.ability,self,stat,user) + end + return true + end + + def pbRaiseStatStageByCause(stat,increment,user,cause,showAnim=true,ignoreContrary=false) + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary && !@battle.moldBreaker + return pbLowerStatStageByCause(stat,increment,user,cause,showAnim,true) + end + # Perform the stat stage change + increment = pbRaiseStatStageBasic(stat,increment,ignoreContrary) + return false if increment<=0 + # Stat up animation and message + @battle.pbCommonAnimation("StatUp",self) if showAnim + if user.index==@index + arrStatTexts = [ + _INTL("{1}'s {2} raised its {3}!",pbThis,cause,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} sharply raised its {3}!",pbThis,cause,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} drastically raised its {3}!",pbThis,cause,GameData::Stat.get(stat).name)] + else + arrStatTexts = [ + _INTL("{1}'s {2} raised {3}'s {4}!",user.pbThis,cause,pbThis(true),GameData::Stat.get(stat).name), + _INTL("{1}'s {2} sharply raised {3}'s {4}!",user.pbThis,cause,pbThis(true),GameData::Stat.get(stat).name), + _INTL("{1}'s {2} drastically raised {3}'s {4}!",user.pbThis,cause,pbThis(true),GameData::Stat.get(stat).name)] + end + @battle.pbDisplay(arrStatTexts[[increment-1,2].min]) + # Trigger abilities upon stat gain + if abilityActive? + BattleHandlers.triggerAbilityOnStatGain(self.ability,self,stat,user) + end + return true + end + + def pbRaiseStatStageByAbility(stat,increment,user,splashAnim=true, abilityName=nil) + return false if fainted? + ret = false + @battle.pbShowAbilitySplash(user,false,true,abilityName) #if splashAnim + if pbCanRaiseStatStage?(stat,user,nil,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + ret = pbRaiseStatStage(stat,increment,user) + else + ret = pbRaiseStatStageByCause(stat,increment,user,user.abilityName) + end + end + @battle.pbHideAbilitySplash(user) if splashAnim + return ret + end + + #============================================================================= + # Decrease stat stages + #============================================================================= + def statStageAtMin?(stat) + return @stages[stat]<=-6 + end + + def pbCanLowerStatStage?(stat,user=nil,move=nil,showFailMsg=false,ignoreContrary=false) + return false if fainted? + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary && !@battle.moldBreaker + return pbCanRaiseStatStage?(stat,user,move,showFailMsg,true) + end + if !user || user.index!=@index # Not self-inflicted + if @effects[PBEffects::Substitute]>0 && !(move && move.ignoresSubstitute?(user)) + @battle.pbDisplay(_INTL("{1} is protected by its substitute!",pbThis)) if showFailMsg + return false + end + if pbOwnSide.effects[PBEffects::Mist]>0 && + !(user && user.hasActiveAbility?(:INFILTRATOR)) + @battle.pbDisplay(_INTL("{1} is protected by Mist!",pbThis)) if showFailMsg + return false + end + if abilityActive? + return false if BattleHandlers.triggerStatLossImmunityAbility( + self.ability,self,stat,@battle,showFailMsg) if !@battle.moldBreaker + return false if BattleHandlers.triggerStatLossImmunityAbilityNonIgnorable( + self.ability,self,stat,@battle,showFailMsg) + end + if !@battle.moldBreaker + eachAlly do |b| + next if !b.abilityActive? + return false if BattleHandlers.triggerStatLossImmunityAllyAbility( + b.ability,b,self,stat,@battle,showFailMsg) + end + end + end + # Check the stat stage + if statStageAtMin?(stat) + @battle.pbDisplay(_INTL("{1}'s {2} won't go any lower!", + pbThis, GameData::Stat.get(stat).name)) if showFailMsg + return false + end + return true + end + + def pbLowerStatStageBasic(stat,increment,ignoreContrary=false) + if !@battle.moldBreaker + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary + return pbRaiseStatStageBasic(stat,increment,true) + end + # Simple + increment *= 2 if hasActiveAbility?(:SIMPLE) + end + # Change the stat stage + increment = [increment,6+@stages[stat]].min + if increment>0 + stat_name = GameData::Stat.get(stat).name + new = @stages[stat]-increment + PBDebug.log("[Stat change] #{pbThis}'s #{stat_name}: #{@stages[stat]} -> #{new} (-#{increment})") + @stages[stat] -= increment + end + return increment + end + + def pbLowerStatStage(stat,increment,user,showAnim=true,ignoreContrary=false) + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary && !@battle.moldBreaker + return pbRaiseStatStage(stat,increment,user,showAnim,true) + end + # Perform the stat stage change + increment = pbLowerStatStageBasic(stat,increment,ignoreContrary) + return false if increment<=0 + # Stat down animation and message + @battle.pbCommonAnimation("StatDown",self) if showAnim + arrStatTexts = [ + _INTL("{1}'s {2} fell!",pbThis,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} harshly fell!",pbThis,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} severely fell!",pbThis,GameData::Stat.get(stat).name)] + @battle.pbDisplay(arrStatTexts[[increment-1,2].min]) + # Trigger abilities upon stat loss + if abilityActive? + BattleHandlers.triggerAbilityOnStatLoss(self.ability,self,stat,user) + end + return true + end + + def pbLowerStatStageByCause(stat,increment,user,cause,showAnim=true,ignoreContrary=false) + # Contrary + if hasActiveAbility?(:CONTRARY) && !ignoreContrary && !@battle.moldBreaker + return pbRaiseStatStageByCause(stat,increment,user,cause,showAnim,true) + end + # Perform the stat stage change + increment = pbLowerStatStageBasic(stat,increment,ignoreContrary) + return false if increment<=0 + # Stat down animation and message + @battle.pbCommonAnimation("StatDown",self) if showAnim + if user.index==@index + arrStatTexts = [ + _INTL("{1}'s {2} lowered its {3}!",pbThis,cause,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} harshly lowered its {3}!",pbThis,cause,GameData::Stat.get(stat).name), + _INTL("{1}'s {2} severely lowered its {3}!",pbThis,cause,GameData::Stat.get(stat).name)] + else + arrStatTexts = [ + _INTL("{1}'s {2} lowered {3}'s {4}!",user.pbThis,cause,pbThis(true),GameData::Stat.get(stat).name), + _INTL("{1}'s {2} harshly lowered {3}'s {4}!",user.pbThis,cause,pbThis(true),GameData::Stat.get(stat).name), + _INTL("{1}'s {2} severely lowered {3}'s {4}!",user.pbThis,cause,pbThis(true),GameData::Stat.get(stat).name)] + end + @battle.pbDisplay(arrStatTexts[[increment-1,2].min]) + # Trigger abilities upon stat loss + if abilityActive? + BattleHandlers.triggerAbilityOnStatLoss(self.ability,self,stat,user) + end + return true + end + + def pbLowerStatStageByAbility(stat,increment,user,splashAnim=true,checkContact=false,ability_name=nil) + ret = false + @battle.pbShowAbilitySplash(user,false ,false ,ability_name) if splashAnim + if pbCanLowerStatStage?(stat,user,nil,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + (!checkContact || affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH)) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + ret = pbLowerStatStage(stat,increment,user) + else + ret = pbLowerStatStageByCause(stat,increment,user,user.abilityName) + end + end + @battle.pbHideAbilitySplash(user) if splashAnim + return ret + end + + def pbLowerAttackStatStageIntimidate(user) + return false if fainted? + # NOTE: Substitute intentially blocks Intimidate even if self has Contrary. + if @effects[PBEffects::Substitute]>0 + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is protected by its substitute!",pbThis)) + else + @battle.pbDisplay(_INTL("{1}'s substitute protected it from {2}'s {3}!", + pbThis,user.pbThis(true),user.abilityName)) + end + return false + end + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + return pbLowerStatStageByAbility(:ATTACK,1,user,false,false ,user.abilityName) + end + # NOTE: These checks exist to ensure appropriate messages are shown if + # Intimidate is blocked somehow (i.e. the messages should mention the + # Intimidate ability by name). + if !hasActiveAbility?(:CONTRARY) + if pbOwnSide.effects[PBEffects::Mist]>0 + @battle.pbDisplay(_INTL("{1} is protected from {2}'s {3} by Mist!", + pbThis,user.pbThis(true),user.abilityName)) + return false + end + if abilityActive? + if BattleHandlers.triggerStatLossImmunityAbility(self.ability,self,:ATTACK,@battle,false) || + BattleHandlers.triggerStatLossImmunityAbilityNonIgnorable(self.ability,self,:ATTACK,@battle,false) + @battle.pbDisplay(_INTL("{1}'s {2} prevented {3}'s {4} from working!", + pbThis,abilityName,user.pbThis(true),user.abilityName)) + return false + end + end + eachAlly do |b| + next if !b.abilityActive? + if BattleHandlers.triggerStatLossImmunityAllyAbility(b.ability,b,self,:ATTACK,@battle,false) + @battle.pbDisplay(_INTL("{1} is protected from {2}'s {3} by {4}'s {5}!", + pbThis,user.pbThis(true),user.abilityName,b.pbThis(true),b.abilityName)) + return false + end + end + end + return false if !pbCanLowerStatStage?(:ATTACK,user) + return pbLowerStatStageByCause(:ATTACK,1,user,user.abilityName) + end + + #============================================================================= + # Reset stat stages + #============================================================================= + def hasAlteredStatStages? + GameData::Stat.each_battle { |s| return true if @stages[s.id] != 0 } + return false + end + + def hasRaisedStatStages? + GameData::Stat.each_battle { |s| return true if @stages[s.id] > 0 } + return false + end + + def hasLoweredStatStages? + GameData::Stat.each_battle { |s| return true if @stages[s.id] < 0 } + return false + end + + def pbResetStatStages + GameData::Stat.each_battle { |s| @stages[s.id] = 0 } + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/006_Battler_AbilityAndItem.rb b/Data/Scripts/011_Battle/001_Battler/006_Battler_AbilityAndItem.rb new file mode 100644 index 000000000..cbb909ecf --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/006_Battler_AbilityAndItem.rb @@ -0,0 +1,303 @@ +class PokeBattle_Battler + #============================================================================= + # Called when a Pokémon (self) is sent into battle or its ability changes. + #============================================================================= + def pbEffectsOnSwitchIn(switchIn=false) + # Healing Wish/Lunar Dance/entry hazards + @battle.pbOnActiveOne(self) if switchIn + # Primal Revert upon entering battle + @battle.pbPrimalReversion(@index) if !fainted? + # Ending primordial weather, checking Trace + pbContinualAbilityChecks(true) + # Abilities that trigger upon switching in + if (!fainted? && unstoppableAbility?) || abilityActive? + BattleHandlers.triggerAbilityOnSwitchIn(self.ability,self,@battle) + end + # Check for end of primordial weather + @battle.pbEndPrimordialWeather + # Items that trigger upon switching in (Air Balloon message) + if switchIn && itemActive? + BattleHandlers.triggerItemOnSwitchIn(self.item,self,@battle) + end + # Berry check, status-curing ability check + pbHeldItemTriggerCheck if switchIn + pbAbilityStatusCureCheck + end + + #============================================================================= + # Ability effects + #============================================================================= + def pbAbilitiesOnSwitchOut + if abilityActive? + BattleHandlers.triggerAbilityOnSwitchOut(self.ability,self,false) + end + # Reset form + @battle.peer.pbOnLeavingBattle(@battle,@pokemon,@battle.usedInBattle[idxOwnSide][@index/2]) + # Treat self as fainted + @hp = 0 + @fainted = true + # Check for end of primordial weather + @battle.pbEndPrimordialWeather + end + + def pbAbilitiesOnFainting + # Self fainted; check all other battlers to see if their abilities trigger + @battle.pbPriority(true).each do |b| + next if !b || !b.abilityActive? + BattleHandlers.triggerAbilityChangeOnBattlerFainting(b.ability,b,self,@battle) + end + @battle.pbPriority(true).each do |b| + next if !b || !b.abilityActive? + BattleHandlers.triggerAbilityOnBattlerFainting(b.ability,b,self,@battle) + end + end + + # Used for Emergency Exit/Wimp Out. + def pbAbilitiesOnDamageTaken(oldHP,newHP=-1) + return false if !abilityActive? + newHP = @hp if newHP<0 + return false if oldHP<@totalhp/2 || newHP>=@totalhp/2 # Didn't drop below half + ret = BattleHandlers.triggerAbilityOnHPDroppedBelowHalf(self.ability,self,@battle) + return ret # Whether self has switched out + end + + # Called when a Pokémon (self) enters battle, at the end of each move used, + # and at the end of each round. + def pbContinualAbilityChecks(onSwitchIn=false) + # Check for end of primordial weather + @battle.pbEndPrimordialWeather + # Trace + if hasActiveAbility?(:TRACE) + # NOTE: In Gen 5 only, Trace only triggers upon the Trace bearer switching + # in and not at any later times, even if a traceable ability turns + # up later. Essentials ignores this, and allows Trace to trigger + # whenever it can even in the old battle mechanics. + choices = [] + @battle.eachOtherSideBattler(@index) do |b| + next if b.ungainableAbility? || + [:POWEROFALCHEMY, :RECEIVER, :TRACE].include?(b.ability_id) + choices.push(b) + end + if choices.length>0 + choice = choices[@battle.pbRandom(choices.length)] + @battle.pbShowAbilitySplash(self) + self.ability = choice.ability + @battle.pbDisplay(_INTL("{1} traced {2}'s {3}!",pbThis,choice.pbThis(true),choice.abilityName)) + @battle.pbHideAbilitySplash(self) + if !onSwitchIn && (unstoppableAbility? || abilityActive?) + BattleHandlers.triggerAbilityOnSwitchIn(self.ability,self,@battle) + end + end + end + end + + #============================================================================= + # Ability curing + #============================================================================= + # Cures status conditions, confusion and infatuation. + def pbAbilityStatusCureCheck + if abilityActive? + BattleHandlers.triggerStatusCureAbility(self.ability,self) + end + end + + #============================================================================= + # Ability change + #============================================================================= + def pbOnAbilityChanged(oldAbil) + if @effects[PBEffects::Illusion] && oldAbil == :ILLUSION + @effects[PBEffects::Illusion] = nil + if !@effects[PBEffects::Transform] + @battle.scene.pbChangePokemon(self, @pokemon) + @battle.pbDisplay(_INTL("{1}'s {2} wore off!", pbThis, GameData::Ability.get(oldAbil).name)) + @battle.pbSetSeen(self) + end + end + @effects[PBEffects::GastroAcid] = false if unstoppableAbility? + @effects[PBEffects::SlowStart] = 0 if self.ability != :SLOWSTART + # Revert form if Flower Gift/Forecast was lost + pbCheckFormOnWeatherChange + # Check for end of primordial weather + @battle.pbEndPrimordialWeather + end + + #============================================================================= + # Held item consuming/removing + #============================================================================= + def canConsumeBerry? + return false if @battle.pbCheckOpposingAbility(:UNNERVE, @index) + return true + end + + def canConsumePinchBerry?(check_gluttony = true) + return false if !canConsumeBerry? + return true if @hp <= @totalhp / 4 + return true if @hp <= @totalhp / 2 && (!check_gluttony || hasActiveAbility?(:GLUTTONY)) + return false + end + + # permanent is whether the item is lost even after battle. Is false for Knock + # Off. + def pbRemoveItem(permanent = true) + @effects[PBEffects::ChoiceBand] = nil + @effects[PBEffects::Unburden] = true if self.item + + if permanent && self.item == self.initialItem + if $PokemonBag.pbQuantity(self.initialItem)>=1 + $PokemonBag.pbDeleteItem(self.initialItem) + else + setInitialItem(nil) + end + end + self.item = nil + end + + def pbConsumeItem(recoverable=true,symbiosis=true,belch=true) + PBDebug.log("[Item consumed] #{pbThis} consumed its held #{itemName}") + if recoverable + setRecycleItem(@item_id) + @effects[PBEffects::PickupItem] = @item_id + @effects[PBEffects::PickupUse] = @battle.nextPickupUse + end + setBelched if belch && self.item.is_berry? + pbRemoveItem + pbSymbiosis if symbiosis + end + + def pbSymbiosis + return if fainted? + return if !self.item + @battle.pbPriority(true).each do |b| + next if b.opposes? + next if !b.hasActiveAbility?(:SYMBIOSIS) + next if !b.item || b.unlosableItem?(b.item) + next if unlosableItem?(b.item) + @battle.pbShowAbilitySplash(b) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} shared its {2} with {3}!", + b.pbThis,b.itemName,pbThis(true))) + else + @battle.pbDisplay(_INTL("{1}'s {2} let it share its {3} with {4}!", + b.pbThis,b.abilityName,b.itemName,pbThis(true))) + end + self.item = b.item + b.item = nil + b.effects[PBEffects::Unburden] = true + @battle.pbHideAbilitySplash(b) + pbHeldItemTriggerCheck + break + end + end + + # item_to_use is an item ID or GameData::Item object. own_item is whether the + # item is held by self. fling is for Fling only. + def pbHeldItemTriggered(item_to_use, own_item = true, fling = false) + # Cheek Pouch + if hasActiveAbility?(:CHEEKPOUCH) && GameData::Item.get(item_to_use).is_berry? && canHeal? + @battle.pbShowAbilitySplash(self) + pbRecoverHP(@totalhp / 3) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1}'s HP was restored.", pbThis)) + else + @battle.pbDisplay(_INTL("{1}'s {2} restored its HP.", pbThis, abilityName)) + end + @battle.pbHideAbilitySplash(self) + end + pbConsumeItem if own_item + pbSymbiosis if !own_item && !fling # Bug Bite/Pluck users trigger Symbiosis + end + + #============================================================================= + # Held item trigger checks + #============================================================================= + # NOTE: A Pokémon using Bug Bite/Pluck, and a Pokémon having an item thrown at + # it via Fling, will gain the effect of the item even if the Pokémon is + # affected by item-negating effects. + # item_to_use is an item ID for Bug Bite/Pluck and Fling, and nil otherwise. + # fling is for Fling only. + def pbHeldItemTriggerCheck(item_to_use = nil, fling = false) + return if fainted? + return if !item_to_use && !itemActive? + pbItemHPHealCheck(item_to_use, fling) + pbItemStatusCureCheck(item_to_use, fling) + pbItemEndOfMoveCheck(item_to_use, fling) + # For Enigma Berry, Kee Berry and Maranga Berry, which have their effects + # when forcibly consumed by Pluck/Fling. + if item_to_use + itm = item_to_use || self.item + if BattleHandlers.triggerTargetItemOnHitPositiveBerry(itm, self, @battle, true) + pbHeldItemTriggered(itm, false, fling) + end + end + end + + # item_to_use is an item ID for Bug Bite/Pluck and Fling, and nil otherwise. + # fling is for Fling only. + def pbItemHPHealCheck(item_to_use = nil, fling = false) + return if !item_to_use && !itemActive? + itm = item_to_use || self.item + if BattleHandlers.triggerHPHealItem(itm, self, @battle, !item_to_use.nil?) + pbHeldItemTriggered(itm, item_to_use.nil?, fling) + elsif !item_to_use + pbItemTerrainStatBoostCheck + end + end + + # Cures status conditions, confusion, infatuation and the other effects cured + # by Mental Herb. + # item_to_use is an item ID for Bug Bite/Pluck and Fling, and nil otherwise. + # fling is for Fling only. + def pbItemStatusCureCheck(item_to_use = nil, fling = false) + return if fainted? + return if !item_to_use && !itemActive? + itm = item_to_use || self.item + if BattleHandlers.triggerStatusCureItem(itm, self, @battle, !item_to_use.nil?) + pbHeldItemTriggered(itm, item_to_use.nil?, fling) + end + end + + # Called at the end of using a move. + # item_to_use is an item ID for Bug Bite/Pluck and Fling, and nil otherwise. + # fling is for Fling only. + def pbItemEndOfMoveCheck(item_to_use = nil, fling = false) + return if fainted? + return if !item_to_use && !itemActive? + itm = item_to_use || self.item + if BattleHandlers.triggerEndOfMoveItem(itm, self, @battle, !item_to_use.nil?) + pbHeldItemTriggered(itm, item_to_use.nil?, fling) + elsif BattleHandlers.triggerEndOfMoveStatRestoreItem(itm, self, @battle, !item_to_use.nil?) + pbHeldItemTriggered(itm, item_to_use.nil?, fling) + end + end + + # Used for White Herb (restore lowered stats). Only called by Moody and Sticky + # Web, as all other stat reduction happens because of/during move usage and + # this handler is also called at the end of each move's usage. + # item_to_use is an item ID for Bug Bite/Pluck and Fling, and nil otherwise. + # fling is for Fling only. + def pbItemStatRestoreCheck(item_to_use = nil, fling = false) + return if fainted? + return if !item_to_use && !itemActive? + itm = item_to_use || self.item + if BattleHandlers.triggerEndOfMoveStatRestoreItem(itm, self, @battle, !item_to_use.nil?) + pbHeldItemTriggered(itm, item_to_use.nil?, fling) + end + end + + # Called when the battle terrain changes and when a Pokémon loses HP. + def pbItemTerrainStatBoostCheck + return if !itemActive? + if BattleHandlers.triggerTerrainStatBoostItem(self.item, self, @battle) + pbHeldItemTriggered(self.item) + end + end + + # Used for Adrenaline Orb. Called when Intimidate is triggered (even if + # Intimidate has no effect on the Pokémon). + def pbItemOnIntimidatedCheck + return if !itemActive? + if BattleHandlers.triggerItemOnIntimidated(self.item, self, @battle) + pbHeldItemTriggered(self.item) + end + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/007_Battler_UseMove.rb b/Data/Scripts/011_Battle/001_Battler/007_Battler_UseMove.rb new file mode 100644 index 000000000..9b5b44903 --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/007_Battler_UseMove.rb @@ -0,0 +1,809 @@ +class PokeBattle_Battler + #============================================================================= + # Turn processing + #============================================================================= + def pbProcessTurn(choice, tryFlee = true) + return false if fainted? + # Wild roaming Pokémon always flee if possible + if tryFlee && @battle.wildBattle? && opposes? && + @battle.rules["alwaysflee"] && @battle.pbCanRun?(@index) + pbBeginTurn(choice) + pbSEPlay("Battle flee") + @battle.pbDisplay(_INTL("{1} fled from battle!", pbThis)) + @battle.decision = 3 + pbEndTurn(choice) + return true + end + # Shift with the battler next to this one + if choice[0] == :Shift + idxOther = -1 + case @battle.pbSideSize(@index) + when 2 + idxOther = (@index + 2) % 4 + when 3 + if @index != 2 && @index != 3 # If not in middle spot already + idxOther = ((@index % 2) == 0) ? 2 : 3 + end + end + if idxOther >= 0 + @battle.pbSwapBattlers(@index, idxOther) + case @battle.pbSideSize(@index) + when 2 + @battle.pbDisplay(_INTL("{1} moved across!", pbThis)) + when 3 + @battle.pbDisplay(_INTL("{1} moved to the center!", pbThis)) + end + end + pbBeginTurn(choice) + pbCancelMoves + @lastRoundMoved = @battle.turnCount # Done something this round + return true + end + # If this battler's action for this round wasn't "use a move" + if choice[0] != :UseMove + # Clean up effects that end at battler's turn + pbBeginTurn(choice) + pbEndTurn(choice) + return false + end + # Turn is skipped if Pursuit was used during switch + if @effects[PBEffects::Pursuit] + @effects[PBEffects::Pursuit] = false + pbCancelMoves + pbEndTurn(choice) + @battle.pbJudge + return false + end + # Use the move + PBDebug.log("[Move usage] #{pbThis} started using #{choice[2].name}") + PBDebug.logonerr { + pbUseMove(choice, choice[2] == @battle.struggle) + } + @battle.pbJudge + # Update priority order + @battle.pbCalculatePriority if Settings::RECALCULATE_TURN_ORDER_AFTER_SPEED_CHANGES + return true + end + + #============================================================================= + # + #============================================================================= + def pbBeginTurn(_choice) + # Cancel some lingering effects which only apply until the user next moves + @effects[PBEffects::BeakBlast] = false + @effects[PBEffects::DestinyBondPrevious] = @effects[PBEffects::DestinyBond] + @effects[PBEffects::DestinyBond] = false + @effects[PBEffects::Grudge] = false + @effects[PBEffects::MoveNext] = false + @effects[PBEffects::Quash] = 0 + # Encore's effect ends if the encored move is no longer available + if @effects[PBEffects::Encore] > 0 && pbEncoredMoveIndex < 0 + @effects[PBEffects::Encore] = 0 + @effects[PBEffects::EncoreMove] = nil + end + end + + # Called when the usage of various multi-turn moves is disrupted due to + # failing pbTryUseMove, being ineffective against all targets, or because + # Pursuit was used specially to intercept a switching foe. + # Cancels the use of multi-turn moves and counters thereof. Note that Hyper + # Beam's effect is NOT cancelled. + def pbCancelMoves(full_cancel = false) + # Outragers get confused anyway if they are disrupted during their final + # turn of using the move + if @effects[PBEffects::Outrage] == 1 && pbCanConfuseSelf?(false) && !full_cancel + pbConfuse(_INTL("{1} became confused due to fatigue!", pbThis)) + end + # Cancel usage of most multi-turn moves + @effects[PBEffects::TwoTurnAttack] = nil + @effects[PBEffects::Rollout] = 0 + @effects[PBEffects::Outrage] = 0 + @effects[PBEffects::Uproar] = 0 + @effects[PBEffects::Bide] = 0 + @currentMove = nil + # Reset counters for moves which increase them when used in succession + @effects[PBEffects::FuryCutter] = 0 + end + + def pbEndTurn(_choice) + @lastRoundMoved = @battle.turnCount # Done something this round + if !@effects[PBEffects::ChoiceBand] && + hasActiveItem?([:CHOICEBAND, :CHOICESPECS, :CHOICESCARF]) + if @lastMoveUsed && pbHasMove?(@lastMoveUsed) + @effects[PBEffects::ChoiceBand] = @lastMoveUsed + elsif @lastRegularMoveUsed && pbHasMove?(@lastRegularMoveUsed) + @effects[PBEffects::ChoiceBand] = @lastRegularMoveUsed + end + end + @effects[PBEffects::BeakBlast] = false + @effects[PBEffects::Charge] = 0 if @effects[PBEffects::Charge] == 1 + @effects[PBEffects::GemConsumed] = nil + @effects[PBEffects::ShellTrap] = false + @battle.eachBattler { |b| b.pbContinualAbilityChecks } # Trace, end primordial weathers + end + + def pbConfusionDamage(msg) + @damageState.reset + @damageState.initialHP = @hp + confusionMove = PokeBattle_Confusion.new(@battle, nil) + confusionMove.calcType = confusionMove.pbCalcType(self) # nil + @damageState.typeMod = confusionMove.pbCalcTypeMod(confusionMove.calcType, self, self) # 8 + confusionMove.pbCheckDamageAbsorption(self, self) + confusionMove.pbCalcDamage(self, self) + confusionMove.pbReduceDamage(self, self) + self.hp -= @damageState.hpLost + confusionMove.pbAnimateHitAndHPLost(self, [self]) + @battle.pbDisplay(msg) # "It hurt itself in its confusion!" + confusionMove.pbRecordDamageLost(self, self) + confusionMove.pbEndureKOMessage(self) + pbFaint if fainted? + pbItemHPHealCheck + end + + #============================================================================= + # Simple "use move" method, used when a move calls another move and for Future + # Sight's attack + #============================================================================= + def pbUseMoveSimple(moveID, target = -1, idxMove = -1, specialUsage = true) + choice = [] + choice[0] = :UseMove # "Use move" + choice[1] = idxMove # Index of move to be used in user's moveset + if idxMove >= 0 + choice[2] = @moves[idxMove] + else + choice[2] = PokeBattle_Move.from_pokemon_move(@battle, Pokemon::Move.new(moveID)) + choice[2].pp = -1 + end + choice[3] = target # Target (-1 means no target yet) + PBDebug.log("[Move usage] #{pbThis} started using the called/simple move #{choice[2].name}") + pbUseMove(choice, specialUsage) + end + + #============================================================================= + # Master "use move" method + #============================================================================= + def pbUseMove(choice, specialUsage = false) + # NOTE: This is intentionally determined before a multi-turn attack can + # set specialUsage to true. + skipAccuracyCheck = (specialUsage && choice[2] != @battle.struggle) + # Start using the move + pbBeginTurn(choice) + # Force the use of certain moves if they're already being used + if usingMultiTurnAttack? + choice[2] = PokeBattle_Move.from_pokemon_move(@battle, Pokemon::Move.new(@currentMove)) + specialUsage = true + elsif @effects[PBEffects::Encore] > 0 && choice[1] >= 0 && + @battle.pbCanShowCommands?(@index) + idxEncoredMove = pbEncoredMoveIndex + if idxEncoredMove >= 0 && @battle.pbCanChooseMove?(@index, idxEncoredMove, false) + if choice[1] != idxEncoredMove # Change move if battler was Encored mid-round + choice[1] = idxEncoredMove + choice[2] = @moves[idxEncoredMove] + choice[3] = -1 # No target chosen + end + end + end + # Labels the move being used as "move" + move = choice[2] + return if !move # if move was not chosen somehow + # Try to use the move (inc. disobedience) + @lastMoveFailed = false + if !pbTryUseMove(choice, move, specialUsage, skipAccuracyCheck) + @lastMoveUsed = nil + @lastMoveUsedType = nil + if !specialUsage + @lastRegularMoveUsed = nil + @lastRegularMoveTarget = -1 + end + @battle.pbGainExp # In case self is KO'd due to confusion + pbCancelMoves + pbEndTurn(choice) + return + end + move = choice[2] # In case disobedience changed the move to be used + return if !move # if move was not chosen somehow + # Subtract PP + if !specialUsage + if !pbReducePP(move) + @battle.pbDisplay(_INTL("{1} used {2}!", pbThis, move.name)) + @battle.pbDisplay(_INTL("But there was no PP left for the move!")) + @lastMoveUsed = nil + @lastMoveUsedType = nil + @lastRegularMoveUsed = nil + @lastRegularMoveTarget = -1 + @lastMoveFailed = true + pbCancelMoves + pbEndTurn(choice) + return + end + end + # Stance Change + if self.ability == :STANCECHANGE + if move.damagingMove? + user = pbFindUser(choice, move) + stanceChangeEffect(user, true) + elsif move.id == :KINGSSHIELD + user = pbFindUser(choice, move) + stanceChangeEffect(user, false) + end + end + # Calculate the move's type during this usage + move.calcType = move.pbCalcType(self) + # Start effect of Mold Breaker + @battle.moldBreaker = hasMoldBreaker? + # Remember that user chose a two-turn move + if move.pbIsChargingTurn?(self) + # Beginning the use of a two-turn attack + @effects[PBEffects::TwoTurnAttack] = move.id + @currentMove = move.id + else + @effects[PBEffects::TwoTurnAttack] = nil # Cancel use of two-turn attack + end + # Add to counters for moves which increase them when used in succession + move.pbChangeUsageCounters(self, specialUsage) + # Charge up Metronome item + if hasActiveItem?(:METRONOME) && !move.callsAnotherMove? + if @lastMoveUsed && @lastMoveUsed == move.id && !@lastMoveFailed + @effects[PBEffects::Metronome] += 1 + else + @effects[PBEffects::Metronome] = 0 + end + end + # Record move as having been used + @lastMoveUsed = move.id + @lastMoveUsedType = move.calcType # For Conversion 2 + if !specialUsage + @lastRegularMoveUsed = move.id # For Disable, Encore, Instruct, Mimic, Mirror Move, Sketch, Spite + @lastRegularMoveTarget = choice[3] # For Instruct (remembering original target is fine) + @movesUsed.push(move.id) if !@movesUsed.include?(move.id) # For Last Resort + end + @battle.lastMoveUsed = move.id # For Copycat + @battle.lastMoveUser = @index # For "self KO" battle clause to avoid draws + @battle.successStates[@index].useState = 1 # Battle Arena - assume failure + # Find the default user (self or Snatcher) and target(s) + user = pbFindUser(choice, move) + user = pbChangeUser(choice, move, user) + targets = pbFindTargets(choice, move, user) + targets = pbChangeTargets(move, user, targets) + # Pressure + if !specialUsage + targets.each do |b| + next unless b.opposes?(user) && b.hasActiveAbility?(:PRESSURE) + PBDebug.log("[Ability triggered] #{b.pbThis}'s #{b.abilityName}") + user.pbReducePP(move) + end + if move.pbTarget(user).affects_foe_side + @battle.eachOtherSideBattler(user) do |b| + next unless b.hasActiveAbility?(:PRESSURE) + PBDebug.log("[Ability triggered] #{b.pbThis}'s #{b.abilityName}") + user.pbReducePP(move) + end + end + end + # Dazzling/Queenly Majesty make the move fail here + @battle.pbPriority(true).each do |b| + next if !b || !b.abilityActive? + if BattleHandlers.triggerMoveBlockingAbility(b.ability, b, user, targets, move, @battle) + @battle.pbDisplayBrief(_INTL("{1} used {2}!", user.pbThis, move.name)) + @battle.pbShowAbilitySplash(b) + @battle.pbDisplay(_INTL("{1} cannot use {2}!", user.pbThis, move.name)) + @battle.pbHideAbilitySplash(b) + user.lastMoveFailed = true + pbCancelMoves + pbEndTurn(choice) + return + end + end + # "X used Y!" message + # Can be different for Bide, Fling, Focus Punch and Future Sight + # NOTE: This intentionally passes self rather than user. The user is always + # self except if Snatched, but this message should state the original + # user (self) even if the move is Snatched. + move.pbDisplayUseMessage(self) + # Snatch's message (user is the new user, self is the original user) + if move.snatched + @lastMoveFailed = true # Intentionally applies to self, not user + @battle.pbDisplay(_INTL("{1} snatched {2}'s move!", user.pbThis, pbThis(true))) + end + # "But it failed!" checks + if move.pbMoveFailed?(user, targets) + PBDebug.log(sprintf("[Move failed] In function code %s's def pbMoveFailed?", move.function)) + user.lastMoveFailed = true + pbCancelMoves + pbEndTurn(choice) + return + end + # Perform set-up actions and display messages + # Messages include Magnitude's number and Pledge moves' "it's a combo!" + move.pbOnStartUse(user, targets) + # Self-thawing due to the move + if user.status == :FROZEN && move.thawsUser? + user.pbCureStatus(false) + @battle.pbDisplay(_INTL("{1} melted the ice!", user.pbThis)) + end + # Powder + if user.effects[PBEffects::Powder] && move.calcType == :FIRE + @battle.pbCommonAnimation("Powder", user) + @battle.pbDisplay(_INTL("When the flame touched the powder on the Pokémon, it exploded!")) + user.lastMoveFailed = true + if ![:Rain, :HeavyRain].include?(@battle.pbWeather) && user.takesIndirectDamage? + oldHP = user.hp + user.pbReduceHP((user.totalhp / 4.0).round, false) + user.pbFaint if user.fainted? + @battle.pbGainExp # In case user is KO'd by this + user.pbItemHPHealCheck + if user.pbAbilitiesOnDamageTaken(oldHP) + user.pbEffectsOnSwitchIn(true) + end + end + pbCancelMoves + pbEndTurn(choice) + return + end + # Primordial Sea, Desolate Land + if move.damagingMove? + case @battle.pbWeather + when :HeavyRain + if move.calcType == :FIRE + @battle.pbDisplay(_INTL("The Fire-type attack fizzled out in the heavy rain!")) + user.lastMoveFailed = true + pbCancelMoves + pbEndTurn(choice) + return + end + when :HarshSun + if move.calcType == :WATER + @battle.pbDisplay(_INTL("The Water-type attack evaporated in the harsh sunlight!")) + user.lastMoveFailed = true + pbCancelMoves + pbEndTurn(choice) + return + end + end + end + # Protean + if user.hasActiveAbility?(:PROTEAN) && !move.callsAnotherMove? && !move.snatched + if user.pbHasOtherType?(move.calcType) && !GameData::Type.get(move.calcType).pseudo_type + @battle.pbShowAbilitySplash(user) + user.pbChangeTypes(move.calcType) + typeName = GameData::Type.get(move.calcType).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", user.pbThis, typeName)) + @battle.pbHideAbilitySplash(user) + # NOTE: The GF games say that if Curse is used by a non-Ghost-type + # Pokémon which becomes Ghost-type because of Protean, it should + # target and curse itself. I think this is silly, so I'm making it + # choose a random opponent to curse instead. + if move.function == "10D" && targets.length == 0 # Curse + choice[3] = -1 + targets = pbFindTargets(choice, move, user) + end + end + end + #--------------------------------------------------------------------------- + magicCoater = -1 + magicBouncer = -1 + if targets.length == 0 && move.pbTarget(user).num_targets > 0 && !move.worksWithNoTargets? + # def pbFindTargets should have found a target(s), but it didn't because + # they were all fainted + # All target types except: None, User, UserSide, FoeSide, BothSides + @battle.pbDisplay(_INTL("But there was no target...")) + user.lastMoveFailed = true + else + # We have targets, or move doesn't use targets + # Reset whole damage state, perform various success checks (not accuracy) + user.initialHP = user.hp + targets.each do |b| + b.damageState.reset + b.damageState.initialHP = b.hp + if !pbSuccessCheckAgainstTarget(move, user, b) + b.damageState.unaffected = true + end + end + # Magic Coat/Magic Bounce checks (for moves which don't target Pokémon) + if targets.length == 0 && move.canMagicCoat? + @battle.pbPriority(true).each do |b| + next if b.fainted? || !b.opposes?(user) + next if b.semiInvulnerable? + if b.effects[PBEffects::MagicCoat] + magicCoater = b.index + b.effects[PBEffects::MagicCoat] = false + break + elsif b.hasActiveAbility?(:MAGICBOUNCE) && !@battle.moldBreaker && + !b.effects[PBEffects::MagicBounce] + magicBouncer = b.index + b.effects[PBEffects::MagicBounce] = true + break + end + end + end + # Get the number of hits + numHits = move.pbNumHits(user, targets) + # Process each hit in turn + realNumHits = 0 + for i in 0...numHits + break if magicCoater >= 0 || magicBouncer >= 0 + success = pbProcessMoveHit(move, user, targets, i, skipAccuracyCheck) + if !success + if i == 0 && targets.length > 0 + hasFailed = false + targets.each do |t| + next if t.damageState.protected + hasFailed = t.damageState.unaffected + break if !t.damageState.unaffected + end + user.lastMoveFailed = hasFailed + end + break + end + realNumHits += 1 + break if user.fainted? + break if [:SLEEP, :FROZEN].include?(user.status) + # NOTE: If a multi-hit move becomes disabled partway through doing those + # hits (e.g. by Cursed Body), the rest of the hits continue as + # normal. + break if !targets.any? { |t| !t.fainted? } # All targets are fainted + end + # Battle Arena only - attack is successful + @battle.successStates[user.index].useState = 2 + if targets.length > 0 + @battle.successStates[user.index].typeMod = 0 + targets.each do |b| + next if b.damageState.unaffected + @battle.successStates[user.index].typeMod += b.damageState.typeMod + end + end + # Effectiveness message for multi-hit moves + # NOTE: No move is both multi-hit and multi-target, and the messages below + # aren't quite right for such a hypothetical move. + if numHits > 1 + if move.damagingMove? + targets.each do |b| + next if b.damageState.unaffected || b.damageState.substitute + move.pbEffectivenessMessage(user, b, targets.length) + end + end + if realNumHits == 1 + @battle.pbDisplay(_INTL("Hit 1 time!")) + elsif realNumHits > 1 + @battle.pbDisplay(_INTL("Hit {1} times!", realNumHits)) + end + end + # Magic Coat's bouncing back (move has targets) + targets.each do |b| + next if b.fainted? + next if !b.damageState.magicCoat && !b.damageState.magicBounce + @battle.pbShowAbilitySplash(b) if b.damageState.magicBounce + @battle.pbDisplay(_INTL("{1} bounced the {2} back!", b.pbThis, move.name)) + @battle.pbHideAbilitySplash(b) if b.damageState.magicBounce + newChoice = choice.clone + newChoice[3] = user.index + newTargets = pbFindTargets(newChoice, move, b) + newTargets = pbChangeTargets(move, b, newTargets) + success = pbProcessMoveHit(move, b, newTargets, 0, false) + b.lastMoveFailed = true if !success + targets.each { |otherB| otherB.pbFaint if otherB && otherB.fainted? } + user.pbFaint if user.fainted? + end + # Magic Coat's bouncing back (move has no targets) + if magicCoater >= 0 || magicBouncer >= 0 + mc = @battle.battlers[(magicCoater >= 0) ? magicCoater : magicBouncer] + if !mc.fainted? + user.lastMoveFailed = true + @battle.pbShowAbilitySplash(mc) if magicBouncer >= 0 + @battle.pbDisplay(_INTL("{1} bounced the {2} back!", mc.pbThis, move.name)) + @battle.pbHideAbilitySplash(mc) if magicBouncer >= 0 + success = pbProcessMoveHit(move, mc, [], 0, false) + mc.lastMoveFailed = true if !success + targets.each { |b| b.pbFaint if b && b.fainted? } + user.pbFaint if user.fainted? + end + end + # Move-specific effects after all hits + targets.each { |b| move.pbEffectAfterAllHits(user, b) } + # Faint if 0 HP + targets.each { |b| b.pbFaint if b && b.fainted? } + user.pbFaint if user.fainted? + # External/general effects after all hits. Eject Button, Shell Bell, etc. + pbEffectsAfterMove(user, targets, move, realNumHits) + end + # End effect of Mold Breaker + @battle.moldBreaker = false + # Gain Exp + @battle.pbGainExp + # Battle Arena only - update skills + @battle.eachBattler { |b| @battle.successStates[b.index].updateSkill } + # Shadow Pokémon triggering Hyper Mode + pbHyperMode if @battle.choices[@index][0] != :None # Not if self is replaced + # End of move usage + pbEndTurn(choice) + # Instruct + @battle.eachBattler do |b| + next if !b.effects[PBEffects::Instruct] || !b.lastMoveUsed + b.effects[PBEffects::Instruct] = false + idxMove = -1 + b.eachMoveWithIndex { |m, i| idxMove = i if m.id == b.lastMoveUsed } + next if idxMove < 0 + oldLastRoundMoved = b.lastRoundMoved + @battle.pbDisplay(_INTL("{1} used the move instructed by {2}!", b.pbThis, user.pbThis(true))) + PBDebug.logonerr { + b.effects[PBEffects::Instructed] = true + b.pbUseMoveSimple(b.lastMoveUsed, b.lastRegularMoveTarget, idxMove, false) + b.effects[PBEffects::Instructed] = false + } + b.lastRoundMoved = oldLastRoundMoved + @battle.pbJudge + return if @battle.decision > 0 + end + # Dancer + if !@effects[PBEffects::Dancer] && !user.lastMoveFailed && realNumHits > 0 && + !move.snatched && magicCoater < 0 && @battle.pbCheckGlobalAbility(:DANCER) && + move.danceMove? + dancers = [] + @battle.pbPriority(true).each do |b| + dancers.push(b) if b.index != user.index && b.hasActiveAbility?(:DANCER) + end + while dancers.length > 0 + nextUser = dancers.pop + oldLastRoundMoved = nextUser.lastRoundMoved + # NOTE: Petal Dance being used because of Dancer shouldn't lock the + # Dancer into using that move, and shouldn't contribute to its + # turn counter if it's already locked into Petal Dance. + oldOutrage = nextUser.effects[PBEffects::Outrage] + nextUser.effects[PBEffects::Outrage] += 1 if nextUser.effects[PBEffects::Outrage] > 0 + oldCurrentMove = nextUser.currentMove + preTarget = choice[3] + preTarget = user.index if nextUser.opposes?(user) || !nextUser.opposes?(preTarget) + @battle.pbShowAbilitySplash(nextUser, true) + @battle.pbHideAbilitySplash(nextUser) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} kept the dance going with {2}!", + nextUser.pbThis, nextUser.abilityName)) + end + PBDebug.logonerr { + nextUser.effects[PBEffects::Dancer] = true + nextUser.pbUseMoveSimple(move.id, preTarget) + nextUser.effects[PBEffects::Dancer] = false + } + nextUser.lastRoundMoved = oldLastRoundMoved + nextUser.effects[PBEffects::Outrage] = oldOutrage + nextUser.currentMove = oldCurrentMove + @battle.pbJudge + return if @battle.decision > 0 + end + end + end + + def stanceChangeEffect(user, attacking = false) + inSwordForm = user.effects[PBEffects::PowerTrick] + if !inSwordForm && attacking + user.effects[PBEffects::PowerTrick] = true + user.attack, user.defense = user.defense, user.attack + user.spatk, user.spdef = user.spdef, user.spatk + + + #changeForm(1,:AEGISLASH) + + @battle.pbDisplay(_INTL("{1} changed to Sword Mode!", pbThis)) + elsif inSwordForm && !attacking + user.effects[PBEffects::PowerTrick] = false + user.attack, user.defense = user.defense, user.attack + user.spatk, user.spdef = user.spdef, user.spatk + + #user.changeForm(nil,:AEGISLASH) + + @battle.pbDisplay(_INTL("{1} changed to Shield Mode!", pbThis)) + end + end + + def ensure_form_has_sprite(pokemon) + GameData::Species.sprite_filename(pokemon.dexNum) + end + + + def playChangeFormAnimation(animation = "UltraBurst2") + @battle.scene.pbChangePokemon(self, @pokemon) + @battle.scene.pbCommonAnimation(animation, self) + @battle.scene.pbRefreshOne(@index) + end + + def changeSpecies(pokemon, speciesToReplace,newSpecies, animation = "UltraBurst2") + if pokemon.isFusion?() + replaceFusionSpecies(pokemon,speciesToReplace,newSpecies) + else + changeSpeciesSpecific(pokemon,newSpecies) + end + playChangeFormAnimation(animation) + end + + + + # def changeUnfusedSpecies(pokemon, newSpecies, animation = "UltraBurst2") + # + # end + + #For meloetta form change + + def changeFormSpecies(oldForm, newForm,animation = "UltraBurst2") + @pokemon.changeFormSpecies(oldForm,newForm) + playChangeFormAnimation(animation) + end + + + def changeForm(newForm, formChangingSpecies, animation = "UltraBurst2") + spriteform_body = newForm if @pokemon.hasBodyOf?(formChangingSpecies) + spriteform_head = newForm if @pokemon.hasHeadOf?(formChangingSpecies) + + #ensure_form_has_sprite(@pokemon) + + if self.isFusion? + current_form_has_custom = customSpriteExistsSpecies(@pokemon.species) + new_form_has_custom = customSpriteExistsForm(@pokemon.species, spriteform_head, spriteform_body) + should_change_sprite = (current_form_has_custom && new_form_has_custom) || !current_form_has_custom + else + should_change_sprite=true + end + + if should_change_sprite + @pokemon.spriteform_body = spriteform_body + @pokemon.spriteform_head = spriteform_head + end + playChangeFormAnimation(animation) + end + + #============================================================================= + # Attack a single target + #============================================================================= + def pbProcessMoveHit(move, user, targets, hitNum, skipAccuracyCheck) + return false if user.fainted? + # For two-turn attacks being used in a single turn + move.pbInitialEffect(user, targets, hitNum) + numTargets = 0 # Number of targets that are affected by this hit + targets.each { |b| b.damageState.resetPerHit } + # Count a hit for Parental Bond (if it applies) + user.effects[PBEffects::ParentalBond] -= 1 if user.effects[PBEffects::ParentalBond] > 0 + # Accuracy check (accuracy/evasion calc) + if hitNum == 0 || move.successCheckPerHit? + targets.each do |b| + next if b.damageState.unaffected + if pbSuccessCheckPerHit(move, user, b, skipAccuracyCheck) + numTargets += 1 + else + b.damageState.missed = true + b.damageState.unaffected = true + end + end + # If failed against all targets + if targets.length > 0 && numTargets == 0 && !move.worksWithNoTargets? + targets.each do |b| + next if !b.damageState.missed || b.damageState.magicCoat + pbMissMessage(move, user, b) + end + move.pbCrashDamage(user) + user.pbItemHPHealCheck + pbCancelMoves + return false + end + end + # If we get here, this hit will happen and do something + #--------------------------------------------------------------------------- + # Calculate damage to deal + if move.pbDamagingMove? + targets.each do |b| + next if b.damageState.unaffected + # Check whether Substitute/Disguise will absorb the damage + move.pbCheckDamageAbsorption(user, b) + # Calculate the damage against b + # pbCalcDamage shows the "eat berry" animation for SE-weakening + # berries, although the message about it comes after the additional + # effect below + move.pbCalcDamage(user, b, targets.length) # Stored in damageState.calcDamage + # Lessen damage dealt because of False Swipe/Endure/etc. + move.pbReduceDamage(user, b) # Stored in damageState.hpLost + end + end + # Show move animation (for this hit) + move.pbShowAnimation(move.id, user, targets, hitNum) + # Type-boosting Gem consume animation/message + if user.effects[PBEffects::GemConsumed] && hitNum == 0 + # NOTE: The consume animation and message for Gems are shown now, but the + # actual removal of the item happens in def pbEffectsAfterMove. + @battle.pbCommonAnimation("UseItem", user) + @battle.pbDisplay(_INTL("The {1} strengthened {2}'s power!", + GameData::Item.get(user.effects[PBEffects::GemConsumed]).name, move.name)) + end + # Messages about missed target(s) (relevant for multi-target moves only) + targets.each do |b| + next if !b.damageState.missed + pbMissMessage(move, user, b) + end + # Deal the damage (to all allies first simultaneously, then all foes + # simultaneously) + if move.pbDamagingMove? + # This just changes the HP amounts and does nothing else + targets.each do |b| + next if b.damageState.unaffected + move.pbInflictHPDamage(b) + end + # Animate the hit flashing and HP bar changes + move.pbAnimateHitAndHPLost(user, targets) + end + # Self-Destruct/Explosion's damaging and fainting of user + move.pbSelfKO(user) if hitNum == 0 + user.pbFaint if user.fainted? + if move.pbDamagingMove? + targets.each do |b| + next if b.damageState.unaffected + # NOTE: This method is also used for the OKHO special message. + move.pbHitEffectivenessMessages(user, b, targets.length) + # Record data about the hit for various effects' purposes + move.pbRecordDamageLost(user, b) + end + # Close Combat/Superpower's stat-lowering, Flame Burst's splash damage, + # and Incinerate's berry destruction + targets.each do |b| + next if b.damageState.unaffected + move.pbEffectWhenDealingDamage(user, b) + end + # Ability/item effects such as Static/Rocky Helmet, and Grudge, etc. + targets.each do |b| + next if b.damageState.unaffected + pbEffectsOnMakingHit(move, user, b) + end + # Disguise/Endure/Sturdy/Focus Sash/Focus Band messages + targets.each do |b| + next if b.damageState.unaffected + move.pbEndureKOMessage(b) + end + # HP-healing held items (checks all battlers rather than just targets + # because Flame Burst's splash damage affects non-targets) + @battle.pbPriority(true).each { |b| b.pbItemHPHealCheck } + # Animate battlers fainting (checks all battlers rather than just targets + # because Flame Burst's splash damage affects non-targets) + @battle.pbPriority(true).each { |b| b.pbFaint if b && b.fainted? } + end + @battle.pbJudgeCheckpoint(user, move) + # Main effect (recoil/drain, etc.) + targets.each do |b| + next if b.damageState.unaffected + move.pbEffectAgainstTarget(user, b) + end + move.pbEffectGeneral(user) + targets.each { |b| b.pbFaint if b && b.fainted? } + user.pbFaint if user.fainted? + # Additional effect + if !user.hasActiveAbility?(:SHEERFORCE) + targets.each do |b| + next if b.damageState.calcDamage == 0 + chance = move.pbAdditionalEffectChance(user, b) + next if chance <= 0 + if @battle.pbRandom(100) < chance + move.pbAdditionalEffect(user, b) + end + end + end + # Make the target flinch (because of an item/ability) + targets.each do |b| + next if b.fainted? + next if b.damageState.calcDamage == 0 || b.damageState.substitute + chance = move.pbFlinchChance(user, b) + next if chance <= 0 + if @battle.pbRandom(100) < chance + PBDebug.log("[Item/ability triggered] #{user.pbThis}'s King's Rock/Razor Fang or Stench") + b.pbFlinch(user) + end + end + # Message for and consuming of type-weakening berries + # NOTE: The "consume held item" animation for type-weakening berries occurs + # during pbCalcDamage above (before the move's animation), but the + # message about it only shows here. + targets.each do |b| + next if b.damageState.unaffected + next if !b.damageState.berryWeakened + @battle.pbDisplay(_INTL("The {1} weakened the damage to {2}!", b.itemName, b.pbThis(true))) + b.pbConsumeItem + end + targets.each { |b| b.pbFaint if b && b.fainted? } + user.pbFaint if user.fainted? + return true + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/008_Battler_UseMove_Targeting.rb b/Data/Scripts/011_Battle/001_Battler/008_Battler_UseMove_Targeting.rb new file mode 100644 index 000000000..c3cd1a6df --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/008_Battler_UseMove_Targeting.rb @@ -0,0 +1,193 @@ +class PokeBattle_Battler + #============================================================================= + # Get move's user + #============================================================================= + def pbFindUser(_choice,_move) + return self + end + + def pbChangeUser(choice,move,user) + # Snatch + move.snatched = false + if move.canSnatch? + newUser = nil; strength = 100 + @battle.eachBattler do |b| + next if b.effects[PBEffects::Snatch]==0 || + b.effects[PBEffects::Snatch]>=strength + next if b.effects[PBEffects::SkyDrop]>=0 + newUser = b + strength = b.effects[PBEffects::Snatch] + end + if newUser + user = newUser + user.effects[PBEffects::Snatch] = 0 + move.snatched = true + @battle.moldBreaker = user.hasMoldBreaker? + choice[3] = -1 # Clear pre-chosen target + end + end + return user + end + + #============================================================================= + # Get move's default target(s) + #============================================================================= + def pbFindTargets(choice,move,user) + preTarget = choice[3] # A target that was already chosen + targets = [] + # Get list of targets + case move.pbTarget(user).id # Curse can change its target type + when :NearAlly + targetBattler = (preTarget>=0) ? @battle.battlers[preTarget] : nil + if !pbAddTarget(targets,user,targetBattler,move) + pbAddTargetRandomAlly(targets,user,move) + end + when :UserOrNearAlly + targetBattler = (preTarget>=0) ? @battle.battlers[preTarget] : nil + if !pbAddTarget(targets,user,targetBattler,move,true,true) + pbAddTarget(targets,user,user,move,true,true) + end + when :UserAndAllies + pbAddTarget(targets,user,user,move,true,true) + @battle.eachSameSideBattler(user.index) { |b| pbAddTarget(targets,user,b,move,false,true) } + when :NearFoe, :NearOther + targetBattler = (preTarget>=0) ? @battle.battlers[preTarget] : nil + if !pbAddTarget(targets,user,targetBattler,move) + if preTarget>=0 && !user.opposes?(preTarget) + pbAddTargetRandomAlly(targets,user,move) + else + pbAddTargetRandomFoe(targets,user,move) + end + end + when :RandomNearFoe + pbAddTargetRandomFoe(targets,user,move) + when :AllNearFoes + @battle.eachOtherSideBattler(user.index) { |b| pbAddTarget(targets,user,b,move) } + when :Foe, :Other + targetBattler = (preTarget>=0) ? @battle.battlers[preTarget] : nil + if !pbAddTarget(targets,user,targetBattler,move,false) + if preTarget>=0 && !user.opposes?(preTarget) + pbAddTargetRandomAlly(targets,user,move,false) + else + pbAddTargetRandomFoe(targets,user,move,false) + end + end + when :AllFoes + @battle.eachOtherSideBattler(user.index) { |b| pbAddTarget(targets,user,b,move,false) } + when :AllNearOthers + @battle.eachBattler { |b| pbAddTarget(targets,user,b,move) } + when :AllBattlers + @battle.eachBattler { |b| pbAddTarget(targets,user,b,move,false,true) } + else + # Used by Counter/Mirror Coat/Metal Burst/Bide + move.pbAddTarget(targets,user) # Move-specific pbAddTarget, not the def below + end + return targets + end + + #============================================================================= + # Redirect attack to another target + #============================================================================= + def pbChangeTargets(move,user,targets) + target_data = move.pbTarget(user) + return targets if @battle.switching # For Pursuit interrupting a switch + return targets if move.cannotRedirect? + return targets if !target_data.can_target_one_foe? || targets.length != 1 + priority = @battle.pbPriority(true) + nearOnly = !target_data.can_choose_distant_target? + # Spotlight (takes priority over Follow Me/Rage Powder/Lightning Rod/Storm Drain) + newTarget = nil; strength = 100 # Lower strength takes priority + priority.each do |b| + next if b.fainted? || b.effects[PBEffects::SkyDrop]>=0 + next if b.effects[PBEffects::Spotlight]==0 || + b.effects[PBEffects::Spotlight]>=strength + next if !b.opposes?(user) + next if nearOnly && !b.near?(user) + newTarget = b + strength = b.effects[PBEffects::Spotlight] + end + if newTarget + PBDebug.log("[Move target changed] #{newTarget.pbThis}'s Spotlight made it the target") + targets = [] + pbAddTarget(targets,user,newTarget,move,nearOnly) + return targets + end + # Follow Me/Rage Powder (takes priority over Lightning Rod/Storm Drain) + newTarget = nil; strength = 100 # Lower strength takes priority + priority.each do |b| + next if b.fainted? || b.effects[PBEffects::SkyDrop]>=0 + next if b.effects[PBEffects::RagePowder] && !user.affectedByPowder? + next if b.effects[PBEffects::FollowMe]==0 || + b.effects[PBEffects::FollowMe]>=strength + next if !b.opposes?(user) + next if nearOnly && !b.near?(user) + newTarget = b + strength = b.effects[PBEffects::FollowMe] + end + if newTarget + PBDebug.log("[Move target changed] #{newTarget.pbThis}'s Follow Me/Rage Powder made it the target") + targets = [] + pbAddTarget(targets,user,newTarget,move,nearOnly) + return targets + end + # Lightning Rod + targets = pbChangeTargetByAbility(:LIGHTNINGROD,:ELECTRIC,move,user,targets,priority,nearOnly) + # Storm Drain + targets = pbChangeTargetByAbility(:STORMDRAIN,:WATER,move,user,targets,priority,nearOnly) + return targets + end + + def pbChangeTargetByAbility(drawingAbility,drawnType,move,user,targets,priority,nearOnly) + return targets if move.calcType != drawnType + return targets if targets[0].hasActiveAbility?(drawingAbility) + priority.each do |b| + next if b.index==user.index || b.index==targets[0].index + next if !b.hasActiveAbility?(drawingAbility) + next if nearOnly && !b.near?(user) + @battle.pbShowAbilitySplash(b) + targets.clear + pbAddTarget(targets,user,b,move,nearOnly) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} took the attack!",b.pbThis)) + else + @battle.pbDisplay(_INTL("{1} took the attack with its {2}!",b.pbThis,b.abilityName)) + end + @battle.pbHideAbilitySplash(b) + break + end + return targets + end + + #============================================================================= + # Register target + #============================================================================= + def pbAddTarget(targets,user,target,move,nearOnly=true,allowUser=false) + return false if !target || (target.fainted? && !move.cannotRedirect?) + return false if !(allowUser && user==target) && nearOnly && !user.near?(target) + targets.each { |b| return true if b.index==target.index } # Already added + targets.push(target) + return true + end + + def pbAddTargetRandomAlly(targets,user,_move,nearOnly=true) + choices = [] + user.eachAlly do |b| + next if nearOnly && !user.near?(b) + pbAddTarget(choices,user,b,nearOnly) + end + if choices.length>0 + pbAddTarget(targets,user,choices[@battle.pbRandom(choices.length)],nearOnly) + end + end + + def pbAddTargetRandomFoe(targets,user,_move,nearOnly=true) + choices = [] + user.eachOpposing do |b| + next if nearOnly && !user.near?(b) + pbAddTarget(choices,user,b,nearOnly) + end + if choices.length>0 + pbAddTarget(targets,user,choices[@battle.pbRandom(choices.length)],nearOnly) + end + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/009_Battler_UseMove_SuccessChecks.rb b/Data/Scripts/011_Battle/001_Battler/009_Battler_UseMove_SuccessChecks.rb new file mode 100644 index 000000000..da2eb5fe2 --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/009_Battler_UseMove_SuccessChecks.rb @@ -0,0 +1,541 @@ +class PokeBattle_Battler + #============================================================================= + # Decide whether the trainer is allowed to tell the Pokémon to use the given + # move. Called when choosing a command for the round. + # Also called when processing the Pokémon's action, because these effects also + # prevent Pokémon action. Relevant because these effects can become active + # earlier in the same round (after choosing the command but before using the + # move) or an unusable move may be called by another move such as Metronome. + #============================================================================= + def pbCanChooseMove?(move,commandPhase,showMessages=true,specialUsage=false) + # Disable + if @effects[PBEffects::DisableMove]==move.id && !specialUsage + if showMessages + msg = _INTL("{1}'s {2} is disabled!",pbThis,move.name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Heal Block + if @effects[PBEffects::HealBlock]>0 && move.healingMove? + if showMessages + msg = _INTL("{1} can't use {2} because of Heal Block!",pbThis,move.name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Gravity + if @battle.field.effects[PBEffects::Gravity]>0 && move.unusableInGravity? + if showMessages + msg = _INTL("{1} can't use {2} because of gravity!",pbThis,move.name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Throat Chop + if @effects[PBEffects::ThroatChop]>0 && move.soundMove? + if showMessages + msg = _INTL("{1} can't use {2} because of Throat Chop!",pbThis,move.name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Choice Band + if @effects[PBEffects::ChoiceBand] + if hasActiveItem?([:CHOICEBAND,:CHOICESPECS,:CHOICESCARF]) && + pbHasMove?(@effects[PBEffects::ChoiceBand]) + if move.id!=@effects[PBEffects::ChoiceBand] + if showMessages + msg = _INTL("{1} allows the use of only {2}!",itemName, + GameData::Move.get(@effects[PBEffects::ChoiceBand]).name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + else + @effects[PBEffects::ChoiceBand] = nil + end + end + # Taunt + if @effects[PBEffects::Taunt]>0 && move.statusMove? + if showMessages + msg = _INTL("{1} can't use {2} after the taunt!",pbThis,move.name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Torment + if @effects[PBEffects::Torment] && !@effects[PBEffects::Instructed] && + @lastMoveUsed && move.id==@lastMoveUsed && move.id!=@battle.struggle.id + if showMessages + msg = _INTL("{1} can't use the same move twice in a row due to the torment!",pbThis) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Imprison + @battle.eachOtherSideBattler(@index) do |b| + next if !b.effects[PBEffects::Imprison] || !b.pbHasMove?(move.id) + if showMessages + msg = _INTL("{1} can't use its sealed {2}!",pbThis,move.name) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Assault Vest (prevents choosing status moves but doesn't prevent + # executing them) + if hasActiveItem?(:ASSAULTVEST) && move.statusMove? && commandPhase + if showMessages + msg = _INTL("The effects of the {1} prevent status moves from being used!", + itemName) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + # Belch + return false if !move.pbCanChooseMove?(self,commandPhase,showMessages) + return true + end + + #============================================================================= + # Obedience check + #============================================================================= + # Return true if Pokémon continues attacking (although it may have chosen to + # use a different move in disobedience), or false if attack stops. + def pbObedienceCheck?(choice) + return true if usingMultiTurnAttack? + return true if choice[0]!=:UseMove + return true if !@battle.internalBattle + return true if !@battle.pbOwnedByPlayer?(@index) + disobedient = false + # Pokémon may be disobedient; calculate if it is + badgeLevel = 10 * (@battle.pbPlayer.badge_count + 1) + badgeLevel = GameData::GrowthRate.max_level if @battle.pbPlayer.badge_count >= 8 + if (@pokemon.foreign?(@battle.pbPlayer) && @level>badgeLevel) || @pokemon.force_disobey + a = ((@level+badgeLevel)*@battle.pbRandom(256)/256).floor + disobedient |= (a>=badgeLevel) + end + disobedient |= !pbHyperModeObedience(choice[2]) + return true if !disobedient + # Pokémon is disobedient; make it do something else + return pbDisobey(choice,badgeLevel) + end + + def pbDisobey(choice,badgeLevel) + move = choice[2] + PBDebug.log("[Disobedience] #{pbThis} disobeyed") + @effects[PBEffects::Rage] = false + # Do nothing if using Snore/Sleep Talk + if @status == :SLEEP && move.usableWhenAsleep? + @battle.pbDisplay(_INTL("{1} ignored orders and kept sleeping!",pbThis)) + return false + end + b = ((@level+badgeLevel)*@battle.pbRandom(256)/256).floor + # Use another move + if b=0 # Intentionally no message here + PBDebug.log("[Move failed] #{pbThis} can't use #{move.name} because of being Sky Dropped") + return false + end + if @effects[PBEffects::HyperBeam]>0 # Intentionally before Truant + @battle.pbDisplay(_INTL("{1} must recharge!",pbThis)) + return false + end + if choice[1]==-2 # Battle Palace + @battle.pbDisplay(_INTL("{1} appears incapable of using its power!",pbThis)) + return false + end + # Skip checking all applied effects that could make self fail doing something + return true if skipAccuracyCheck + # Check status problems and continue their effects/cure them + case @status + when :SLEEP + self.statusCount -= 1 + if @statusCount<=0 + pbCureStatus + else + pbContinueStatus + if !move.usableWhenAsleep? # Snore/Sleep Talk + @lastMoveFailed = true + return false + end + end + when :FROZEN + if !move.thawsUser? + if @battle.pbRandom(100)<20 + pbCureStatus + else + pbContinueStatus + @lastMoveFailed = true + return false + end + end + end + # Obedience check + return false if !pbObedienceCheck?(choice) + # Truant + if hasActiveAbility?(:TRUANT) + @effects[PBEffects::Truant] = !@effects[PBEffects::Truant] + if !@effects[PBEffects::Truant] # True means loafing, but was just inverted + @battle.pbShowAbilitySplash(self) + @battle.pbDisplay(_INTL("{1} is loafing around!",pbThis)) + @lastMoveFailed = true + @battle.pbHideAbilitySplash(self) + return false + end + end + # Flinching + if @effects[PBEffects::Flinch] + @battle.pbDisplay(_INTL("{1} flinched and couldn't move!",pbThis)) + if abilityActive? + BattleHandlers.triggerAbilityOnFlinch(self.ability,self,@battle) + end + @lastMoveFailed = true + return false + end + # Confusion + if @effects[PBEffects::Confusion]>0 + @effects[PBEffects::Confusion] -= 1 + if @effects[PBEffects::Confusion]<=0 + pbCureConfusion + @battle.pbDisplay(_INTL("{1} snapped out of its confusion.",pbThis)) + else + @battle.pbCommonAnimation("Confusion",self) + @battle.pbDisplay(_INTL("{1} is confused!",pbThis)) + threshold = (Settings::MECHANICS_GENERATION >= 7) ? 33 : 50 # % chance + if @battle.pbRandom(100)=0 + @battle.pbCommonAnimation("Attract",self) + @battle.pbDisplay(_INTL("{1} is in love with {2}!",pbThis, + @battle.battlers[@effects[PBEffects::Attract]].pbThis(true))) + if @battle.pbRandom(100)<50 + @battle.pbDisplay(_INTL("{1} is immobilized by love!",pbThis)) + @lastMoveFailed = true + return false + end + end + return true + end + + #============================================================================= + # Initial success check against the target. Done once before the first hit. + # Includes move-specific failure conditions, protections and type immunities. + #============================================================================= + def pbSuccessCheckAgainstTarget(move,user,target) + typeMod = move.pbCalcTypeMod(move.calcType,user,target) + target.damageState.typeMod = typeMod + # Two-turn attacks can't fail here in the charging turn + return true if user.effects[PBEffects::TwoTurnAttack] + # Move-specific failures + return false if move.pbFailsAgainstTarget?(user,target) + # Immunity to priority moves because of Psychic Terrain + if @battle.field.terrain == :Psychic && target.affectedByTerrain? && target.opposes?(user) && + @battle.choices[user.index][4]>0 # Move priority saved from pbCalculatePriority + @battle.pbDisplay(_INTL("{1} surrounds itself with psychic terrain!",target.pbThis)) + return false + end + # Crafty Shield + if target.pbOwnSide.effects[PBEffects::CraftyShield] && user.index!=target.index && + move.statusMove? && !move.pbTarget(user).targets_all + @battle.pbCommonAnimation("CraftyShield",target) + @battle.pbDisplay(_INTL("Crafty Shield protected {1}!",target.pbThis(true))) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + return false + end + # Wide Guard + if target.pbOwnSide.effects[PBEffects::WideGuard] && user.index!=target.index && + move.pbTarget(user).num_targets > 1 && + (Settings::MECHANICS_GENERATION >= 7 || move.damagingMove?) + @battle.pbCommonAnimation("WideGuard",target) + @battle.pbDisplay(_INTL("Wide Guard protected {1}!",target.pbThis(true))) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + return false + end + if move.canProtectAgainst? + # Quick Guard + if target.pbOwnSide.effects[PBEffects::QuickGuard] && + @battle.choices[user.index][4]>0 # Move priority saved from pbCalculatePriority + @battle.pbCommonAnimation("QuickGuard",target) + @battle.pbDisplay(_INTL("Quick Guard protected {1}!",target.pbThis(true))) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + return false + end + # Protect + if target.effects[PBEffects::Protect] + @battle.pbCommonAnimation("Protect",target) + @battle.pbDisplay(_INTL("{1} protected itself!",target.pbThis)) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + return false + end + # King's Shield + if target.effects[PBEffects::KingsShield] && move.damagingMove? + @battle.pbCommonAnimation("KingsShield",target) + @battle.pbDisplay(_INTL("{1} protected itself!",target.pbThis)) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + if move.pbContactMove?(user) && user.affectedByContactEffect? + if user.pbCanLowerStatStage?(:ATTACK) + user.pbLowerStatStage(:ATTACK,2,nil) + end + end + return false + end + # Spiky Shield + if target.effects[PBEffects::SpikyShield] + @battle.pbCommonAnimation("SpikyShield",target) + @battle.pbDisplay(_INTL("{1} protected itself!",target.pbThis)) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + if move.pbContactMove?(user) && user.affectedByContactEffect? + @battle.scene.pbDamageAnimation(user) + user.pbReduceHP(user.totalhp/8,false) + @battle.pbDisplay(_INTL("{1} was hurt!",user.pbThis)) + user.pbItemHPHealCheck + end + return false + end + # Baneful Bunker + if target.effects[PBEffects::BanefulBunker] + @battle.pbCommonAnimation("BanefulBunker",target) + @battle.pbDisplay(_INTL("{1} protected itself!",target.pbThis)) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + if move.pbContactMove?(user) && user.affectedByContactEffect? + user.pbPoison(target) if user.pbCanPoison?(target,false) + end + return false + end + # Mat Block + if target.pbOwnSide.effects[PBEffects::MatBlock] && move.damagingMove? + # NOTE: Confirmed no common animation for this effect. + @battle.pbDisplay(_INTL("{1} was blocked by the kicked-up mat!",move.name)) + target.damageState.protected = true + @battle.successStates[user.index].protected = true + return false + end + end + # Magic Coat/Magic Bounce + if move.canMagicCoat? && !target.semiInvulnerable? && target.opposes?(user) + if target.effects[PBEffects::MagicCoat] + target.damageState.magicCoat = true + target.effects[PBEffects::MagicCoat] = false + return false + end + if target.hasActiveAbility?(:MAGICBOUNCE) && !@battle.moldBreaker && + !target.effects[PBEffects::MagicBounce] + target.damageState.magicBounce = true + target.effects[PBEffects::MagicBounce] = true + return false + end + end + # Immunity because of ability (intentionally before type immunity check) + return false if move.pbImmunityByAbility(user,target) + # Type immunity + if move.pbDamagingMove? && Effectiveness.ineffective?(typeMod) + PBDebug.log("[Target immune] #{target.pbThis}'s type immunity") + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + return false + end + # Dark-type immunity to moves made faster by Prankster + if Settings::MECHANICS_GENERATION >= 7 && user.effects[PBEffects::Prankster] && + target.pbHasType?(:DARK) && target.opposes?(user) + PBDebug.log("[Target immune] #{target.pbThis} is Dark-type and immune to Prankster-boosted moves") + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + return false + end + # Airborne-based immunity to Ground moves + if move.damagingMove? && move.calcType == :GROUND && + target.airborne? && !move.hitsFlyingTargets? + if target.hasActiveAbility?(:LEVITATE) && !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} avoided the attack!",target.pbThis)) + else + @battle.pbDisplay(_INTL("{1} avoided the attack with {2}!",target.pbThis,target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return false + end + if target.hasActiveItem?(:AIRBALLOON) + @battle.pbDisplay(_INTL("{1}'s {2} makes Ground moves miss!",target.pbThis,target.itemName)) + return false + end + if target.effects[PBEffects::MagnetRise]>0 + @battle.pbDisplay(_INTL("{1} makes Ground moves miss with Magnet Rise!",target.pbThis)) + return false + end + if target.effects[PBEffects::Telekinesis]>0 + @battle.pbDisplay(_INTL("{1} makes Ground moves miss with Telekinesis!",target.pbThis)) + return false + end + end + # Immunity to powder-based moves + if move.powderMove? + if target.pbHasType?(:GRASS) && Settings::MORE_TYPE_EFFECTS + PBDebug.log("[Target immune] #{target.pbThis} is Grass-type and immune to powder-based moves") + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + return false + end + if Settings::MECHANICS_GENERATION >= 6 + if target.hasActiveAbility?(:OVERCOAT) && !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + @battle.pbDisplay(_INTL("It doesn't affect {1} because of its {2}.",target.pbThis(true),target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return false + end + if target.hasActiveItem?(:SAFETYGOGGLES) + PBDebug.log("[Item triggered] #{target.pbThis} has Safety Goggles and is immune to powder-based moves") + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + return false + end + end + end + # Substitute + if target.effects[PBEffects::Substitute]>0 && move.statusMove? && + !move.ignoresSubstitute?(user) && user.index!=target.index + PBDebug.log("[Target immune] #{target.pbThis} is protected by its Substitute") + @battle.pbDisplay(_INTL("{1} avoided the attack!",target.pbThis(true))) + return false + end + return true + end + + #============================================================================= + # Per-hit success check against the target. + # Includes semi-invulnerable move use and accuracy calculation. + #============================================================================= + def pbSuccessCheckPerHit(move,user,target,skipAccuracyCheck) + # Two-turn attacks can't fail here in the charging turn + return true if user.effects[PBEffects::TwoTurnAttack] + # Lock-On + return true if user.effects[PBEffects::LockOn]>0 && + user.effects[PBEffects::LockOnPos]==target.index + # Toxic + return true if move.pbOverrideSuccessCheckPerHit(user,target) + miss = false; hitsInvul = false + # No Guard + hitsInvul = true if user.hasActiveAbility?(:NOGUARD) || + target.hasActiveAbility?(:NOGUARD) + # Future Sight + hitsInvul = true if @battle.futureSight + # Helping Hand + hitsInvul = true if move.function=="09C" + if !hitsInvul + # Semi-invulnerable moves + if target.effects[PBEffects::TwoTurnAttack] + if target.inTwoTurnAttack?("0C9","0CC","0CE") # Fly, Bounce, Sky Drop + miss = true if !move.hitsFlyingTargets? + elsif target.inTwoTurnAttack?("0CA") # Dig + miss = true if !move.hitsDiggingTargets? + elsif target.inTwoTurnAttack?("0CB") # Dive + miss = true if !move.hitsDivingTargets? + elsif target.inTwoTurnAttack?("0CD","14D") # Shadow Force, Phantom Force + miss = true + end + end + if target.effects[PBEffects::SkyDrop]>=0 && + target.effects[PBEffects::SkyDrop]!=user.index + miss = true if !move.hitsFlyingTargets? + end + end + if user.hasActiveItem?(:MANKEYPAW) + miss = true if rand(2)==1 + end + if !miss + # Called by another move + return true if skipAccuracyCheck + # Accuracy check + return true if move.pbAccuracyCheck(user,target) # Includes Counter/Mirror Coat + end + # Missed + PBDebug.log("[Move failed] Failed pbAccuracyCheck or target is semi-invulnerable") + return false + end + + #============================================================================= + # Message shown when a move fails the per-hit success check above. + #============================================================================= + def pbMissMessage(move,user,target) + if move.pbTarget(user).num_targets > 1 + @battle.pbDisplay(_INTL("{1} avoided the attack!",target.pbThis)) + elsif target.effects[PBEffects::TwoTurnAttack] + @battle.pbDisplay(_INTL("{1} avoided the attack!",target.pbThis)) + elsif !move.pbMissMessage(user,target) + @battle.pbDisplay(_INTL("{1}'s attack missed!",user.pbThis)) + end + end +end diff --git a/Data/Scripts/011_Battle/001_Battler/010_Battler_UseMove_TriggerEffects.rb b/Data/Scripts/011_Battle/001_Battler/010_Battler_UseMove_TriggerEffects.rb new file mode 100644 index 000000000..dcc21b57c --- /dev/null +++ b/Data/Scripts/011_Battle/001_Battler/010_Battler_UseMove_TriggerEffects.rb @@ -0,0 +1,194 @@ +class PokeBattle_Battler + #============================================================================= + # Effect per hit + #============================================================================= + # + + def triggerAbilityEffectsOnHit(move,user,target) + # Target's ability + if target.abilityActive?(true) + oldHP = user.hp + BattleHandlers.triggerTargetAbilityOnHit(target.ability,user,target,move,@battle) + user.pbItemHPHealCheck if user.hp0 && !target.damageState.substitute + triggerAbilityEffectsOnHit(move,user,target) + # Target's item + if target.itemActive?(true) + oldHP = user.hp + BattleHandlers.triggerTargetItemOnHit(target.item,user,target,move,@battle) + user.pbItemHPHealCheck if user.hp0 && !target.damageState.substitute && move.physicalMove? + target.tookPhysicalHit = true + target.effects[PBEffects::MoveNext] = true + target.effects[PBEffects::Quash] = 0 + end + end + # Grudge + if target.effects[PBEffects::Grudge] && target.fainted? + move.pp = 0 + @battle.pbDisplay(_INTL("{1}'s {2} lost all of its PP due to the grudge!", + user.pbThis,move.name)) + end + # Destiny Bond (recording that it should apply) + if target.effects[PBEffects::DestinyBond] && target.fainted? + if user.effects[PBEffects::DestinyBondTarget]<0 + user.effects[PBEffects::DestinyBondTarget] = target.index + end + end + end + end + + #============================================================================= + # Effects after all hits (i.e. at end of move usage) + #============================================================================= + def pbEffectsAfterMove(user,targets,move,numHits) + # Defrost + if move.damagingMove? + targets.each do |b| + next if b.damageState.unaffected || b.damageState.substitute + next if b.status != :FROZEN + # NOTE: Non-Fire-type moves that thaw the user will also thaw the + # target (in Gen 6+). + if move.calcType == :FIRE || (Settings::MECHANICS_GENERATION >= 6 && move.thawsUser?) + b.pbCureStatus + end + end + end + # Destiny Bond + # NOTE: Although Destiny Bond is similar to Grudge, they don't apply at + # the same time (although Destiny Bond does check whether it's going + # to trigger at the same time as Grudge). + if user.effects[PBEffects::DestinyBondTarget]>=0 && !user.fainted? + dbName = @battle.battlers[user.effects[PBEffects::DestinyBondTarget]].pbThis + @battle.pbDisplay(_INTL("{1} took its attacker down with it!",dbName)) + user.pbReduceHP(user.hp,false) + user.pbItemHPHealCheck + user.pbFaint + @battle.pbJudgeCheckpoint(user) + end + # User's ability + if user.abilityActive? + BattleHandlers.triggerUserAbilityEndOfMove(user.ability,user,targets,move,@battle) + BattleHandlers.triggerUserAbilityEndOfMove(user.ability2,user,targets,move,@battle) if user.ability2 + end + # Greninja - Battle Bond + if !user.fainted? && !user.effects[PBEffects::Transform] && + user.isSpecies?(:GRENINJA) && user.ability == :BATTLEBOND + if !@battle.pbAllFainted?(user.idxOpposingSide) && + !@battle.battleBond[user.index&1][user.pokemonIndex] + numFainted = 0 + targets.each { |b| numFainted += 1 if b.damageState.fainted } + if numFainted>0 && user.form==1 + @battle.battleBond[user.index&1][user.pokemonIndex] = true + @battle.pbDisplay(_INTL("{1} became fully charged due to its bond with its Trainer!",user.pbThis)) + @battle.pbShowAbilitySplash(user,true) + @battle.pbHideAbilitySplash(user) + user.pbChangeForm(2,_INTL("{1} became Ash-Greninja!",user.pbThis)) + end + end + end + # Consume user's Gem + if user.effects[PBEffects::GemConsumed] + # NOTE: The consume animation and message for Gems are shown immediately + # after the move's animation, but the item is only consumed now. + user.pbConsumeItem + end + # Pokémon switching caused by Roar, Whirlwind, Circle Throw, Dragon Tail + switchedBattlers = [] + move.pbSwitchOutTargetsEffect(user,targets,numHits,switchedBattlers) + # Target's item, user's item, target's ability (all negated by Sheer Force) + if move.addlEffect==0 || !user.hasActiveAbility?(:SHEERFORCE) + pbEffectsAfterMove2(user,targets,move,numHits,switchedBattlers) + end + # Some move effects that need to happen here, i.e. U-turn/Volt Switch + # switching, Baton Pass switching, Parting Shot switching, Relic Song's form + # changing, Fling/Natural Gift consuming item. + if !switchedBattlers.include?(user.index) + move.pbEndOfMoveUsageEffect(user,targets,numHits,switchedBattlers) + end + if numHits>0 + @battle.eachBattler { |b| b.pbItemEndOfMoveCheck } + end + end + + # Everything in this method is negated by Sheer Force. + def pbEffectsAfterMove2(user,targets,move,numHits,switchedBattlers) + hpNow = user.hp # Intentionally determined now, before Shell Bell + # Target's held item (Eject Button, Red Card) + switchByItem = [] + @battle.pbPriority(true).each do |b| + next if !targets.any? { |targetB| targetB.index==b.index } + next if b.damageState.unaffected || b.damageState.calcDamage==0 || + switchedBattlers.include?(b.index) + next if !b.itemActive? + BattleHandlers.triggerTargetItemAfterMoveUse(b.item,b,user,move,switchByItem,@battle) + end + @battle.moldBreaker = false if switchByItem.include?(user.index) + @battle.pbPriority(true).each do |b| + b.pbEffectsOnSwitchIn(true) if switchByItem.include?(b.index) + end + switchByItem.each { |idxB| switchedBattlers.push(idxB) } + # User's held item (Life Orb, Shell Bell) + if !switchedBattlers.include?(user.index) && user.itemActive? + BattleHandlers.triggerUserItemAfterMoveUse(user.item,user,targets,move,numHits,@battle) + end + # Target's ability (Berserk, Color Change, Emergency Exit, Pickpocket, Wimp Out) + switchWimpOut = [] + @battle.pbPriority(true).each do |b| + next if !targets.any? { |targetB| targetB.index==b.index } + next if b.damageState.unaffected || switchedBattlers.include?(b.index) + next if !b.abilityActive? + BattleHandlers.triggerTargetAbilityAfterMoveUse(b.ability,b,user,move,switchedBattlers,@battle) + BattleHandlers.triggerTargetAbilityAfterMoveUse(b.ability2,b,user,move,switchedBattlers,@battle) if b.ability2 + if !switchedBattlers.include?(b.index) && move.damagingMove? + if b.pbAbilitiesOnDamageTaken(b.damageState.initialHP) # Emergency Exit, Wimp Out + switchWimpOut.push(b.index) + end + end + end + @battle.moldBreaker = false if switchWimpOut.include?(user.index) + @battle.pbPriority(true).each do |b| + next if b.index==user.index + b.pbEffectsOnSwitchIn(true) if switchWimpOut.include?(b.index) + end + switchWimpOut.each { |idxB| switchedBattlers.push(idxB) } + # User's ability (Emergency Exit, Wimp Out) + if !switchedBattlers.include?(user.index) && move.damagingMove? + hpNow = user.hp if user.hp= 7) + itemName = GameData::Item.get(item).name + battle.pbCommonAnimation("EatBerry",battler) if !forced + fraction_to_heal = 8 # Gens 6 and lower + if Settings::MECHANICS_GENERATION == 7; fraction_to_heal = 2 + elsif Settings::MECHANICS_GENERATION >= 8; fraction_to_heal = 3 + end + amt = battler.pbRecoverHP(battler.totalhp / fraction_to_heal) + if amt>0 + if forced + PBDebug.log("[Item triggered] Forced consuming of #{itemName}") + battle.pbDisplay(_INTL("{1}'s HP was restored.",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1} restored its health using its {2}!",battler.pbThis,itemName)) + end + end + flavor_stat = [:ATTACK, :DEFENSE, :SPEED, :SPECIAL_ATTACK, :SPECIAL_DEFENSE][flavor] + battler.nature.stat_changes.each do |change| + next if change[1] > 0 || change[0] != flavor_stat + battle.pbDisplay(confuseMsg) + battler.pbConfuse if battler.pbCanConfuseSelf?(false) + break + end + return true +end + +def pbBattleStatIncreasingBerry(battler,battle,item,forced,stat,increment=1) + return false if !forced && !battler.canConsumePinchBerry? + return false if !battler.pbCanRaiseStatStage?(stat,battler) + itemName = GameData::Item.get(item).name + if forced + PBDebug.log("[Item triggered] Forced consuming of #{itemName}") + return battler.pbRaiseStatStage(stat,increment,battler) + end + battle.pbCommonAnimation("EatBerry",battler) + return battler.pbRaiseStatStageByCause(stat,increment,battler,itemName) +end + +# For abilities that grant immunity to moves of a particular type, and raises +# one of the ability's bearer's stats instead. +def pbBattleMoveImmunityStatAbility(user,target,move,moveType,immuneType,stat,increment,battle) + return false if user.index==target.index + return false if moveType != immuneType + battle.pbShowAbilitySplash(target) + if target.pbCanRaiseStatStage?(stat,target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + target.pbRaiseStatStage(stat,increment,target) + else + target.pbRaiseStatStageByCause(stat,increment,target,target.abilityName) + end + else + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1}'s {2} made {3} ineffective!", + target.pbThis,target.abilityName,move.name)) + end + end + battle.pbHideAbilitySplash(target) + return true +end + +# For abilities that grant immunity to moves of a particular type, and heals the +# ability's bearer by 1/4 of its total HP instead. +def pbBattleMoveImmunityHealAbility(user,target,move,moveType,immuneType,battle) + return false if user.index==target.index + return false if moveType != immuneType + battle.pbShowAbilitySplash(target) + if target.canHeal? && target.pbRecoverHP(target.totalhp/4)>0 + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s HP was restored.",target.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} restored its HP.",target.pbThis,target.abilityName)) + end + else + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1}'s {2} made {3} ineffective!", + target.pbThis,target.abilityName,move.name)) + end + end + battle.pbHideAbilitySplash(target) + return true +end + +def pbBattleGem(user,type,move,mults,moveType) + # Pledge moves never consume Gems + return if move.is_a?(PokeBattle_PledgeMove) + return if moveType != type + user.effects[PBEffects::GemConsumed] = user.item_id + if Settings::MECHANICS_GENERATION >= 6 + mults[:base_damage_multiplier] *= 1.3 + else + mults[:base_damage_multiplier] *= 1.5 + end +end + +def pbBattleTypeWeakingBerry(type,moveType,target,mults) + return if moveType != type + return if Effectiveness.resistant?(target.damageState.typeMod) && moveType != :NORMAL + mults[:final_damage_multiplier] /= 2 + target.damageState.berryWeakened = true + target.battle.pbCommonAnimation("EatBerry",target) +end + +def pbBattleWeatherAbility(weather,battler,battle,ignorePrimal=false) + return if !ignorePrimal && [:HarshSun, :HeavyRain, :StrongWinds].include?(battle.field.weather) + return if battle.field.weather==weather + battle.pbShowAbilitySplash(battler) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s {2} activated!",battler.pbThis,battler.abilityName)) + end + fixedDuration = false + fixedDuration = true if Settings::FIXED_DURATION_WEATHER_FROM_ABILITY && + ![:HarshSun, :HeavyRain, :StrongWinds].include?(weather) + battle.pbStartWeather(battler,weather,fixedDuration) + # NOTE: The ability splash is hidden again in def pbStartWeather. +end diff --git a/Data/Scripts/011_Battle/002_Move/001_PokeBattle_Move.rb b/Data/Scripts/011_Battle/002_Move/001_PokeBattle_Move.rb new file mode 100644 index 000000000..964c48e06 --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/001_PokeBattle_Move.rb @@ -0,0 +1,141 @@ +class PokeBattle_Move + attr_reader :battle + attr_reader :realMove + attr_accessor :id + attr_reader :name + attr_reader :function + attr_reader :baseDamage + attr_reader :type + attr_reader :category + attr_reader :accuracy + attr_accessor :pp + attr_writer :total_pp + attr_reader :addlEffect + attr_reader :target + attr_reader :priority + attr_reader :flags + attr_accessor :calcType + attr_accessor :powerBoost + attr_accessor :snatched + + def to_int; return @id; end + + #============================================================================= + # Creating a move + #============================================================================= + def initialize(battle, move) + @battle = battle + @realMove = move + @id = move.id + @name = move.name # Get the move's name + # Get data on the move + @function = move.function_code + @baseDamage = move.base_damage + @type = move.type + @category = move.category + @accuracy = move.accuracy + @pp = move.pp # Can be changed with Mimic/Transform + @addlEffect = move.effect_chance + @target = move.target + @priority = move.priority + @flags = move.flags + @calcType = nil + @powerBoost = false # For Aerilate, Pixilate, Refrigerate, Galvanize + @snatched = false + end + + # This is the code actually used to generate a PokeBattle_Move object. The + # object generated is a subclass of this one which depends on the move's + # function code (found in the script section PokeBattle_MoveEffect). + def PokeBattle_Move.from_pokemon_move(battle, move) + validate move => Pokemon::Move + moveFunction = move.function_code || "000" + className = sprintf("PokeBattle_Move_%s", moveFunction) + if Object.const_defined?(className) + return Object.const_get(className).new(battle, move) + end + return PokeBattle_UnimplementedMove.new(battle, move) + end + + #============================================================================= + # About the move + #============================================================================= + def pbTarget(_user); return GameData::Target.get(@target); end + + def total_pp + return @total_pp if @total_pp && @total_pp>0 # Usually undefined + return @realMove.total_pp if @realMove + return 0 + end + + # NOTE: This method is only ever called while using a move (and also by the + # AI), so using @calcType here is acceptable. + def physicalMove?(thisType=nil) + return (@category==0) if Settings::MOVE_CATEGORY_PER_MOVE + thisType ||= @calcType + thisType ||= @type + return true if !thisType + return GameData::Type.get(thisType).physical? + end + + # NOTE: This method is only ever called while using a move (and also by the + # AI), so using @calcType here is acceptable. + def specialMove?(thisType=nil) + return (@category==1) if Settings::MOVE_CATEGORY_PER_MOVE + thisType ||= @calcType + thisType ||= @type + return false if !thisType + return GameData::Type.get(thisType).special? + end + + def damagingMove?; return @category!=2; end + def statusMove?; return @category==2; end + + def usableWhenAsleep?; return false; end + def unusableInGravity?; return false; end + def healingMove?; return false; end + def recoilMove?; return false; end + def flinchingMove?; return false; end + def callsAnotherMove?; return false; end + # Whether the move can/will hit more than once in the same turn (including + # Beat Up which may instead hit just once). Not the same as pbNumHits>1. + def multiHitMove?; return false; end + def chargingTurnMove?; return false; end + def successCheckPerHit?; return false; end + def hitsFlyingTargets?; return false; end + def hitsDiggingTargets?; return false; end + def hitsDivingTargets?; return false; end + def ignoresReflect?; return false; end # For Brick Break + def cannotRedirect?; return false; end # For Future Sight/Doom Desire + def worksWithNoTargets?; return false; end # For Explosion + def damageReducedByBurn?; return true; end # For Facade + def triggersHyperMode?; return false; end + + def contactMove?; return @flags[/a/]; end + def canProtectAgainst?; return @flags[/b/]; end + def canMagicCoat?; return @flags[/c/]; end + def canSnatch?; return @flags[/d/]; end + def canMirrorMove?; return @flags[/e/]; end + def canKingsRock?; return @flags[/f/]; end + def thawsUser?; return @flags[/g/]; end + def highCriticalRate?; return @flags[/h/]; end + def bitingMove?; return @flags[/i/]; end + def punchingMove?; return @flags[/j/]; end + def soundMove?; return @flags[/k/]; end + def powderMove?; return @flags[/l/]; end + def pulseMove?; return @flags[/m/]; end + def bombMove?; return @flags[/n/]; end + def danceMove?; return @flags[/o/]; end + + # Causes perfect accuracy (param=1) and double damage (param=2). + def tramplesMinimize?(_param=1); return false; end + def nonLethal?(_user,_target); return false; end # For False Swipe + + def ignoresSubstitute?(user) # user is the Pokémon using this move + if Settings::MECHANICS_GENERATION >= 6 + return true if soundMove? + return true if user && user.hasActiveAbility?(:INFILTRATOR) + end + return false + end +end diff --git a/Data/Scripts/011_Battle/002_Move/002_Move_Usage.rb b/Data/Scripts/011_Battle/002_Move/002_Move_Usage.rb new file mode 100644 index 000000000..3891acda5 --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/002_Move_Usage.rb @@ -0,0 +1,350 @@ +class PokeBattle_Move + #============================================================================= + # Effect methods per move usage + #============================================================================= + def pbCanChooseMove?(user,commandPhase,showMessages); return true; end # For Belch + def pbDisplayChargeMessage(user); end # For Focus Punch/shell Trap/Beak Blast + def pbOnStartUse(user,targets); end + def pbAddTarget(targets,user); end # For Counter, etc. and Bide + + # Reset move usage counters (child classes can increment them). + def pbChangeUsageCounters(user,specialUsage) + user.effects[PBEffects::FuryCutter] = 0 + user.effects[PBEffects::ParentalBond] = 0 + user.effects[PBEffects::ProtectRate] = 1 + @battle.field.effects[PBEffects::FusionBolt] = false + @battle.field.effects[PBEffects::FusionFlare] = false + end + + def pbDisplayUseMessage(user) + @battle.pbDisplayBrief(_INTL("{1} used {2}!",user.pbThis,@name)) + end + + def pbMissMessage(user,target); return false; end + + #============================================================================= + # + #============================================================================= + # Whether the move is currently in the "charging" turn of a two turn attack. + # Is false if Power Herb or another effect lets a two turn move charge and + # attack in the same turn. + # user.effects[PBEffects::TwoTurnAttack] is set to the move's ID during the + # charging turn, and is nil during the attack turn. + def pbIsChargingTurn?(user); return false; end + def pbDamagingMove?; return damagingMove?; end + + def pbContactMove?(user) + return false if user.hasActiveAbility?(:LONGREACH) + return contactMove? + end + + # The maximum number of hits in a round this move will actually perform. This + # can be 1 for Beat Up, and can be 2 for any moves affected by Parental Bond. + def pbNumHits(user,targets) + if user.hasActiveAbility?(:PARENTALBOND) && pbDamagingMove? && + !chargingTurnMove? && targets.length==1 + # Record that Parental Bond applies, to weaken the second attack + user.effects[PBEffects::ParentalBond] = 3 + return 2 + end + return 1 + end + + #============================================================================= + # Effect methods per hit + #============================================================================= + def pbOverrideSuccessCheckPerHit(user,target); return false; end + def pbCrashDamage(user); end + def pbInitialEffect(user,targets,hitNum); end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + return if !showAnimation + if user.effects[PBEffects::ParentalBond]==1 + @battle.pbCommonAnimation("ParentalBond",user,targets) + else + @battle.pbAnimation(id,user,targets,hitNum) + end + end + + def pbSelfKO(user); end + def pbEffectWhenDealingDamage(user,target); end + def pbEffectAgainstTarget(user,target); end + def pbEffectGeneral(user); end + def pbAdditionalEffect(user,target); end + def pbEffectAfterAllHits(user,target); end # Move effects that occur after all hits + def pbSwitchOutTargetsEffect(user,targets,numHits,switchedBattlers); end + def pbEndOfMoveUsageEffect(user,targets,numHits,switchedBattlers); end + + #============================================================================= + # Check if target is immune to the move because of its ability + #============================================================================= + def pbImmunityByAbility(user,target) + return false if @battle.moldBreaker + ret = false + if target.abilityActive? + ret = BattleHandlers.triggerMoveImmunityTargetAbility(target.ability, + user,target,self,@calcType,@battle) + end + return ret + end + + #============================================================================= + # Move failure checks + #============================================================================= + # Check whether the move fails completely due to move-specific requirements. + def pbMoveFailed?(user,targets); return false; end + # Checks whether the move will be ineffective against the target. + def pbFailsAgainstTarget?(user,target); return false; end + + def pbMoveFailedLastInRound?(user) + unmoved = false + @battle.eachBattler do |b| + next if b.index==user.index + next if @battle.choices[b.index][0]!=:UseMove && @battle.choices[b.index][0]!=:Shift + next if b.movedThisRound? + unmoved = true + break + end + if !unmoved + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbMoveFailedTargetAlreadyMoved?(target) + if (@battle.choices[target.index][0]!=:UseMove && + @battle.choices[target.index][0]!=:Shift) || target.movedThisRound? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbMoveFailedAromaVeil?(user,target,showMessage=true) + return false if @battle.moldBreaker + if target.hasActiveAbility?(:AROMAVEIL) + if showMessage + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!",target.pbThis)) + else + @battle.pbDisplay(_INTL("{1} is unaffected because of its {2}!", + target.pbThis,target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + end + return true + end + target.eachAlly do |b| + next if !b.hasActiveAbility?(:AROMAVEIL) + if showMessage + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!",target.pbThis)) + else + @battle.pbDisplay(_INTL("{1} is unaffected because of {2}'s {3}!", + target.pbThis,b.pbThis(true),b.abilityName)) + end + @battle.pbHideAbilitySplash(target) + end + return true + end + return false + end + + #============================================================================= + # Weaken the damage dealt (doesn't actually change a battler's HP) + #============================================================================= + def pbCheckDamageAbsorption(user,target) + # Substitute will take the damage + if target.effects[PBEffects::Substitute]>0 && !ignoresSubstitute?(user) && + (!user || user.index!=target.index) + target.damageState.substitute = true + return + end + # Disguise will take the damage + if !@battle.moldBreaker && target.isFusionOf(:MIMIKYU) && + target.form==0 && target.ability == :DISGUISE + target.damageState.disguise = true + return + end + end + + def pbReduceDamage(user,target) + damage = target.damageState.calcDamage + # Substitute takes the damage + if target.damageState.substitute + damage = target.effects[PBEffects::Substitute] if damage>target.effects[PBEffects::Substitute] + target.damageState.hpLost = damage + target.damageState.totalHPLost += damage + return + end + # Disguise takes the damage + return if target.damageState.disguise + # Target takes the damage + if damage>=target.hp + damage = target.hp + # Survive a lethal hit with 1 HP effects + if nonLethal?(user,target) + damage -= 1 + elsif target.effects[PBEffects::Endure] + target.damageState.endured = true + damage -= 1 + elsif damage==target.totalhp + if target.hasActiveAbility?(:STURDY) && !@battle.moldBreaker + target.damageState.sturdy = true + damage -= 1 + elsif target.hasActiveItem?(:FOCUSSASH) && target.hp==target.totalhp + target.damageState.focusSash = true + damage -= 1 + elsif target.hasActiveItem?(:FOCUSBAND) && @battle.pbRandom(100)<10 + target.damageState.focusBand = true + damage -= 1 + end + end + end + damage = 0 if damage<0 + target.damageState.hpLost = damage + target.damageState.totalHPLost += damage + end + + #============================================================================= + # Change the target's HP by the amount calculated above + #============================================================================= + def pbInflictHPDamage(target) + if target.damageState.substitute + target.effects[PBEffects::Substitute] -= target.damageState.hpLost + else + target.hp -= target.damageState.hpLost + end + end + + #============================================================================= + # Animate the damage dealt, including lowering the HP + #============================================================================= + # Animate being damaged and losing HP (by a move) + def pbAnimateHitAndHPLost(user,targets) + # Animate allies first, then foes + animArray = [] + for side in 0...2 # side here means "allies first, then foes" + targets.each do |b| + next if b.damageState.unaffected || b.damageState.hpLost==0 + next if (side==0 && b.opposes?(user)) || (side==1 && !b.opposes?(user)) + oldHP = b.hp+b.damageState.hpLost + PBDebug.log("[Move damage] #{b.pbThis} lost #{b.damageState.hpLost} HP (#{oldHP}=>#{b.hp})") + effectiveness = 0 + if Effectiveness.resistant?(b.damageState.typeMod); effectiveness = 1 + elsif Effectiveness.super_effective?(b.damageState.typeMod); effectiveness = 2 + end + animArray.push([b,oldHP,effectiveness]) + end + if animArray.length>0 + @battle.scene.pbHitAndHPLossAnimation(animArray) + animArray.clear + end + end + end + + #============================================================================= + # Messages upon being hit + #============================================================================= + def pbEffectivenessMessage(user,target,numTargets=1) + return if target.damageState.disguise + if Effectiveness.super_effective?(target.damageState.typeMod) + if numTargets>1 + @battle.pbDisplay(_INTL("It's super effective on {1}!",target.pbThis(true))) + else + @battle.pbDisplay(_INTL("It's super effective!")) + end + elsif Effectiveness.not_very_effective?(target.damageState.typeMod) + if numTargets>1 + @battle.pbDisplay(_INTL("It's not very effective on {1}...",target.pbThis(true))) + else + @battle.pbDisplay(_INTL("It's not very effective...")) + end + end + end + + def pbHitEffectivenessMessages(user,target,numTargets=1) + return if target.damageState.disguise + if target.damageState.substitute + @battle.pbDisplay(_INTL("The substitute took damage for {1}!",target.pbThis(true))) + end + if target.damageState.critical + if numTargets>1 + @battle.pbDisplay(_INTL("A critical hit on {1}!",target.pbThis(true))) + else + @battle.pbDisplay(_INTL("A critical hit!")) + end + end + # Effectiveness message, for moves with 1 hit + if !multiHitMove? && user.effects[PBEffects::ParentalBond]==0 + pbEffectivenessMessage(user,target,numTargets) + end + if target.damageState.substitute && target.effects[PBEffects::Substitute]==0 + target.effects[PBEffects::Substitute] = 0 + @battle.pbDisplay(_INTL("{1}'s substitute faded!",target.pbThis)) + end + end + + def pbEndureKOMessage(target) + if target.damageState.disguise + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("Its disguise served it as a decoy!")) + else + @battle.pbDisplay(_INTL("{1}'s disguise served it as a decoy!",target.pbThis)) + end + @battle.pbHideAbilitySplash(target) + target.pbChangeForm(1,_INTL("{1}'s disguise was busted!",target.pbThis)) + elsif target.damageState.endured + @battle.pbDisplay(_INTL("{1} endured the hit!",target.pbThis)) + elsif target.damageState.sturdy + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} endured the hit!",target.pbThis)) + else + @battle.pbDisplay(_INTL("{1} hung on with Sturdy!",target.pbThis)) + end + @battle.pbHideAbilitySplash(target) + elsif target.damageState.focusSash + @battle.pbCommonAnimation("UseItem",target) + @battle.pbDisplay(_INTL("{1} hung on using its Focus Sash!",target.pbThis)) + target.pbConsumeItem + elsif target.damageState.focusBand + @battle.pbCommonAnimation("UseItem",target) + @battle.pbDisplay(_INTL("{1} hung on using its Focus Band!",target.pbThis)) + end + end + + # Used by Counter/Mirror Coat/Metal Burst/Revenge/Focus Punch/Bide/Assurance. + def pbRecordDamageLost(user,target) + damage = target.damageState.hpLost + # NOTE: In Gen 3 where a move's category depends on its type, Hidden Power + # is for some reason countered by Counter rather than Mirror Coat, + # regardless of its calculated type. Hence the following two lines of + # code. + moveType = nil + moveType = :NORMAL if @function=="090" # Hidden Power + if physicalMove?(moveType) + target.effects[PBEffects::Counter] = damage + target.effects[PBEffects::CounterTarget] = user.index + elsif specialMove?(moveType) + target.effects[PBEffects::MirrorCoat] = damage + target.effects[PBEffects::MirrorCoatTarget] = user.index + end + if target.effects[PBEffects::Bide]>0 + target.effects[PBEffects::BideDamage] += damage + target.effects[PBEffects::BideTarget] = user.index + end + target.damageState.fainted = true if target.fainted? + target.lastHPLost = damage # For Focus Punch + target.tookDamage = true if damage>0 # For Assurance + target.lastAttacker.push(user.index) # For Revenge + if target.opposes?(user) + target.lastHPLostFromFoe = damage # For Metal Burst + target.lastFoeAttacker.push(user.index) # For Metal Burst + end + end +end diff --git a/Data/Scripts/011_Battle/002_Move/003_Move_Usage_Calculations.rb b/Data/Scripts/011_Battle/002_Move/003_Move_Usage_Calculations.rb new file mode 100644 index 000000000..1ff5f6b13 --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/003_Move_Usage_Calculations.rb @@ -0,0 +1,492 @@ +class PokeBattle_Move + #============================================================================= + # Move's type calculation + #============================================================================= + def pbBaseType(user) + ret = @type + if ret && user.abilityActive? + ret = BattleHandlers.triggerMoveBaseTypeModifierAbility(user.ability,user,self,ret) + end + return ret + end + + def pbCalcType(user) + @powerBoost = false + ret = pbBaseType(user) + if ret && GameData::Type.exists?(:ELECTRIC) + if @battle.field.effects[PBEffects::IonDeluge] && ret == :NORMAL + ret = :ELECTRIC + @powerBoost = false + end + if user.effects[PBEffects::Electrify] + ret = :ELECTRIC + @powerBoost = false + end + end + return ret + end + + #============================================================================= + # Type effectiveness calculation + #============================================================================= + def pbCalcTypeModSingle(moveType,defType,user,target) + ret = Effectiveness.calculate_one(moveType, defType) + # Ring Target + if target.hasActiveItem?(:RINGTARGET) + ret = Effectiveness::NORMAL_EFFECTIVE_ONE if Effectiveness.ineffective_type?(moveType, defType) + end + # Foresight + if user.hasActiveAbility?(:SCRAPPY) || target.effects[PBEffects::Foresight] + ret = Effectiveness::NORMAL_EFFECTIVE_ONE if defType == :GHOST && + Effectiveness.ineffective_type?(moveType, defType) + end + # Miracle Eye + if target.effects[PBEffects::MiracleEye] + ret = Effectiveness::NORMAL_EFFECTIVE_ONE if defType == :DARK && + Effectiveness.ineffective_type?(moveType, defType) + end + # Delta Stream's weather + if @battle.pbWeather == :StrongWinds + ret = Effectiveness::NORMAL_EFFECTIVE_ONE if defType == :FLYING && + Effectiveness.super_effective_type?(moveType, defType) + end + # Grounded Flying-type Pokémon become susceptible to Ground moves + if !target.airborne? + ret = Effectiveness::NORMAL_EFFECTIVE_ONE if defType == :FLYING && moveType == :GROUND + end + return ret + end + + def pbCalcTypeMod(moveType,user,target) + return Effectiveness::NORMAL_EFFECTIVE if !moveType + return Effectiveness::NORMAL_EFFECTIVE if moveType == :GROUND && + target.pbHasType?(:FLYING) && target.hasActiveItem?(:IRONBALL) + # Determine types + tTypes = target.pbTypes(true) + # Get effectivenesses + typeMods = [Effectiveness::NORMAL_EFFECTIVE_ONE] * 3 # 3 types max + if moveType == :SHADOW + if target.shadowPokemon? + typeMods[0] = Effectiveness::NOT_VERY_EFFECTIVE_ONE + else + typeMods[0] = Effectiveness::SUPER_EFFECTIVE_ONE + end + else + tTypes.each_with_index do |type,i| + typeMods[i] = pbCalcTypeModSingle(moveType,type,user,target) + end + end + # Multiply all effectivenesses together + ret = 1 + typeMods.each { |m| ret *= m } + return ret + end + + #============================================================================= + # Accuracy check + #============================================================================= + def pbBaseAccuracy(user,target); return @accuracy; end + + # Accuracy calculations for one-hit KO moves and "always hit" moves are + # handled elsewhere. + def pbAccuracyCheck(user,target) + # "Always hit" effects and "always hit" accuracy + return true if target.effects[PBEffects::Telekinesis]>0 + return true if target.effects[PBEffects::Minimize] && tramplesMinimize?(1) + baseAcc = pbBaseAccuracy(user,target) + return true if baseAcc==0 + # Calculate all multiplier effects + modifiers = {} + modifiers[:base_accuracy] = baseAcc + modifiers[:accuracy_stage] = user.stages[:ACCURACY] + modifiers[:evasion_stage] = target.stages[:EVASION] + modifiers[:accuracy_multiplier] = 1.0 + modifiers[:evasion_multiplier] = 1.0 + pbCalcAccuracyModifiers(user,target,modifiers) + # Check if move can't miss + return true if modifiers[:base_accuracy] == 0 + # Calculation + accStage = [[modifiers[:accuracy_stage], -6].max, 6].min + 6 + evaStage = [[modifiers[:evasion_stage], -6].max, 6].min + 6 + stageMul = [3,3,3,3,3,3, 3, 4,5,6,7,8,9] + stageDiv = [9,8,7,6,5,4, 3, 3,3,3,3,3,3] + accuracy = 100.0 * stageMul[accStage] / stageDiv[accStage] + evasion = 100.0 * stageMul[evaStage] / stageDiv[evaStage] + accuracy = (accuracy * modifiers[:accuracy_multiplier]).round + evasion = (evasion * modifiers[:evasion_multiplier]).round + evasion = 1 if evasion < 1 + # Calculation + return @battle.pbRandom(100) < modifiers[:base_accuracy] * accuracy / evasion + end + + def pbCalcAccuracyModifiers(user,target,modifiers) + # Ability effects that alter accuracy calculation + if user.abilityActive? + BattleHandlers.triggerAccuracyCalcUserAbility(user.ability, + modifiers,user,target,self,@calcType) + end + user.eachAlly do |b| + next if !b.abilityActive? + BattleHandlers.triggerAccuracyCalcUserAllyAbility(b.ability, + modifiers,user,target,self,@calcType) + end + if target.abilityActive? && !@battle.moldBreaker + BattleHandlers.triggerAccuracyCalcTargetAbility(target.ability, + modifiers,user,target,self,@calcType) + end + # Item effects that alter accuracy calculation + if user.itemActive? + BattleHandlers.triggerAccuracyCalcUserItem(user.item, + modifiers,user,target,self,@calcType) + end + if target.itemActive? + BattleHandlers.triggerAccuracyCalcTargetItem(target.item, + modifiers,user,target,self,@calcType) + end + # Other effects, inc. ones that set accuracy_multiplier or evasion_stage to + # specific values + if @battle.field.effects[PBEffects::Gravity] > 0 + modifiers[:accuracy_multiplier] *= 5 / 3.0 + end + if user.effects[PBEffects::MicleBerry] + user.effects[PBEffects::MicleBerry] = false + modifiers[:accuracy_multiplier] *= 1.2 + end + modifiers[:evasion_stage] = 0 if target.effects[PBEffects::Foresight] && modifiers[:evasion_stage] > 0 + modifiers[:evasion_stage] = 0 if target.effects[PBEffects::MiracleEye] && modifiers[:evasion_stage] > 0 + end + + #============================================================================= + # Critical hit check + #============================================================================= + # Return values: + # -1: Never a critical hit. + # 0: Calculate normally. + # 1: Always a critical hit. + def pbCritialOverride(user,target); return 0; end + + # Returns whether the move will be a critical hit. + def pbIsCritical?(user,target) + return false if target.pbOwnSide.effects[PBEffects::LuckyChant]>0 + # Set up the critical hit ratios + ratios = (Settings::NEW_CRITICAL_HIT_RATE_MECHANICS) ? [24,8,2,1] : [16,8,4,3,2] + c = 0 + # Ability effects that alter critical hit rate + if c>=0 && user.abilityActive? + c = BattleHandlers.triggerCriticalCalcUserAbility(user.ability,user,target,c) + end + if c>=0 && target.abilityActive? && !@battle.moldBreaker + c = BattleHandlers.triggerCriticalCalcTargetAbility(target.ability,user,target,c) + end + # Item effects that alter critical hit rate + if c>=0 && user.itemActive? + c = BattleHandlers.triggerCriticalCalcUserItem(user.item,user,target,c) + end + if c>=0 && target.itemActive? + c = BattleHandlers.triggerCriticalCalcTargetItem(target.item,user,target,c) + end + return false if c<0 + # Move-specific "always/never a critical hit" effects + case pbCritialOverride(user,target) + when 1 then return true + when -1 then return false + end + # Other effects + return true if user.hasActiveItem?(:MANKEYPAW) + return true if c>50 # Merciless + return true if user.effects[PBEffects::LaserFocus]>0 + c += 1 if highCriticalRate? + c += user.effects[PBEffects::FocusEnergy] + c += 1 if user.inHyperMode? && @type == :SHADOW + c = ratios.length-1 if c>=ratios.length + # Calculation + return @battle.pbRandom(ratios[c])==0 + end + + #============================================================================= + # Damage calculation + #============================================================================= + def pbBaseDamage(baseDmg,user,target); return baseDmg; end + def pbBaseDamageMultiplier(damageMult,user,target); return damageMult; end + def pbModifyDamage(damageMult,user,target); return damageMult; end + + def pbGetAttackStats(user,target) + if specialMove? + return user.spatk, user.stages[:SPECIAL_ATTACK]+6 + end + return user.attack, user.stages[:ATTACK]+6 + end + + def pbGetDefenseStats(user,target) + if specialMove? + return target.spdef, target.stages[:SPECIAL_DEFENSE]+6 + end + return target.defense, target.stages[:DEFENSE]+6 + end + + def pbCalcDamage(user,target,numTargets=1) + return if statusMove? + if target.damageState.disguise + target.damageState.calcDamage = 1 + return + end + stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8] + stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2] + # Get the move's type + type = @calcType # nil is treated as physical + # Calculate whether this hit deals critical damage + target.damageState.critical = pbIsCritical?(user,target) + # Calcuate base power of move + baseDmg = pbBaseDamage(@baseDamage,user,target) + # Calculate user's attack stat + atk, atkStage = pbGetAttackStats(user,target) + if !target.hasActiveAbility?(:UNAWARE) || @battle.moldBreaker + atkStage = 6 if target.damageState.critical && atkStage<6 + atk = (atk.to_f*stageMul[atkStage]/stageDiv[atkStage]).floor + end + # Calculate target's defense stat + defense, defStage = pbGetDefenseStats(user,target) + if !user.hasActiveAbility?(:UNAWARE) + defStage = 6 if target.damageState.critical && defStage>6 + defense = (defense.to_f*stageMul[defStage]/stageDiv[defStage]).floor + end + # Calculate all multiplier effects + multipliers = { + :base_damage_multiplier => 1.0, + :attack_multiplier => 1.0, + :defense_multiplier => 1.0, + :final_damage_multiplier => 1.0 + } + pbCalcDamageMultipliers(user,target,numTargets,type,baseDmg,multipliers) + # Main damage calculation + baseDmg = [(baseDmg * multipliers[:base_damage_multiplier]).round, 1].max + atk = [(atk * multipliers[:attack_multiplier]).round, 1].max + defense = [(defense * multipliers[:defense_multiplier]).round, 1].max + damage = (((2.0 * user.level / 5 + 2).floor * baseDmg * atk / defense).floor / 50).floor + 2 + damage = [(damage * multipliers[:final_damage_multiplier]).round, 1].max + target.damageState.calcDamage = damage + end + + def pbCalcDamageMultipliers(user,target,numTargets,type,baseDmg,multipliers) + # Global abilities + if (@battle.pbCheckGlobalAbility(:DARKAURA) && type == :DARK) || + (@battle.pbCheckGlobalAbility(:FAIRYAURA) && type == :FAIRY) + if @battle.pbCheckGlobalAbility(:AURABREAK) + multipliers[:base_damage_multiplier] *= 2 / 3.0 + else + multipliers[:base_damage_multiplier] *= 4 / 3.0 + end + end + # Ability effects that alter damage + if user.abilityActive? + BattleHandlers.triggerDamageCalcUserAbility(user.ability, + user,target,self,multipliers,baseDmg,type) + end + if !@battle.moldBreaker + # NOTE: It's odd that the user's Mold Breaker prevents its partner's + # beneficial abilities (i.e. Flower Gift boosting Atk), but that's + # how it works. + user.eachAlly do |b| + next if !b.abilityActive? + BattleHandlers.triggerDamageCalcUserAllyAbility(b.ability, + user,target,self,multipliers,baseDmg,type) + end + if target.abilityActive? + BattleHandlers.triggerDamageCalcTargetAbility(target.ability, + user,target,self,multipliers,baseDmg,type) if !@battle.moldBreaker + BattleHandlers.triggerDamageCalcTargetAbilityNonIgnorable(target.ability, + user,target,self,multipliers,baseDmg,type) + end + target.eachAlly do |b| + next if !b.abilityActive? + BattleHandlers.triggerDamageCalcTargetAllyAbility(b.ability, + user,target,self,multipliers,baseDmg,type) + end + end + # Item effects that alter damage + if user.itemActive? + BattleHandlers.triggerDamageCalcUserItem(user.item, + user,target,self,multipliers,baseDmg,type) + end + if target.itemActive? + BattleHandlers.triggerDamageCalcTargetItem(target.item, + user,target,self,multipliers,baseDmg,type) + end + # Parental Bond's second attack + if user.effects[PBEffects::ParentalBond]==1 + multipliers[:base_damage_multiplier] /= 4 + end + # Other + if user.effects[PBEffects::MeFirst] + multipliers[:base_damage_multiplier] *= 1.5 + end + if user.effects[PBEffects::HelpingHand] && !self.is_a?(PokeBattle_Confusion) + multipliers[:base_damage_multiplier] *= 1.5 + end + if user.effects[PBEffects::Charge]>0 && type == :ELECTRIC + multipliers[:base_damage_multiplier] *= 2 + end + # Mud Sport + if type == :ELECTRIC + @battle.eachBattler do |b| + next if !b.effects[PBEffects::MudSport] + multipliers[:base_damage_multiplier] /= 3 + break + end + if @battle.field.effects[PBEffects::MudSportField]>0 + multipliers[:base_damage_multiplier] /= 3 + end + end + # Water Sport + if type == :FIRE + @battle.eachBattler do |b| + next if !b.effects[PBEffects::WaterSport] + multipliers[:base_damage_multiplier] /= 3 + break + end + if @battle.field.effects[PBEffects::WaterSportField]>0 + multipliers[:base_damage_multiplier] /= 3 + end + end + # Terrain moves + case @battle.field.terrain + when :Electric + multipliers[:base_damage_multiplier] *= 1.5 if type == :ELECTRIC && user.affectedByTerrain? + when :Grassy + multipliers[:base_damage_multiplier] *= 1.5 if type == :GRASS && user.affectedByTerrain? + when :Psychic + multipliers[:base_damage_multiplier] *= 1.5 if type == :PSYCHIC && user.affectedByTerrain? + when :Misty + multipliers[:base_damage_multiplier] /= 2 if type == :DRAGON && target.affectedByTerrain? + end + # Badge multipliers + if @battle.internalBattle + if user.pbOwnedByPlayer? + if physicalMove? && @battle.pbPlayer.badge_count >= Settings::NUM_BADGES_BOOST_ATTACK + multipliers[:attack_multiplier] *= 1.1 + elsif specialMove? && @battle.pbPlayer.badge_count >= Settings::NUM_BADGES_BOOST_SPATK + multipliers[:attack_multiplier] *= 1.1 + end + end + if target.pbOwnedByPlayer? + if physicalMove? && @battle.pbPlayer.badge_count >= Settings::NUM_BADGES_BOOST_DEFENSE + multipliers[:defense_multiplier] *= 1.1 + elsif specialMove? && @battle.pbPlayer.badge_count >= Settings::NUM_BADGES_BOOST_SPDEF + multipliers[:defense_multiplier] *= 1.1 + end + end + end + # Multi-targeting attacks + if numTargets>1 + multipliers[:final_damage_multiplier] *= 0.75 + end + # Weather + case @battle.pbWeather + when :Sun, :HarshSun + if type == :FIRE + multipliers[:final_damage_multiplier] *= 1.5 + elsif type == :WATER + multipliers[:final_damage_multiplier] /= 2 + end + when :Rain, :HeavyRain + if type == :FIRE + multipliers[:final_damage_multiplier] /= 2 + elsif type == :WATER + multipliers[:final_damage_multiplier] *= 1.5 + end + when :Sandstorm + if target.pbHasType?(:ROCK) && specialMove? && @function != "122" # Psyshock + multipliers[:defense_multiplier] *= 1.5 + end + end + # Critical hits + if target.damageState.critical + if Settings::NEW_CRITICAL_HIT_RATE_MECHANICS + multipliers[:final_damage_multiplier] *= 1.5 + else + multipliers[:final_damage_multiplier] *= 2 + end + end + # Random variance + if !self.is_a?(PokeBattle_Confusion) + random = 85+@battle.pbRandom(16) + multipliers[:final_damage_multiplier] *= random / 100.0 + end + # STAB + if type && user.pbHasType?(type) + if user.hasActiveAbility?(:ADAPTABILITY) + multipliers[:final_damage_multiplier] *= 2 + else + multipliers[:final_damage_multiplier] *= 1.5 + end + end + # Type effectiveness + multipliers[:final_damage_multiplier] *= target.damageState.typeMod.to_f / Effectiveness::NORMAL_EFFECTIVE + # Burn + if user.status == :BURN && physicalMove? && damageReducedByBurn? && + !user.hasActiveAbility?(:GUTS) + multipliers[:final_damage_multiplier] /= 2 + end + # Aurora Veil, Reflect, Light Screen + if !ignoresReflect? && !target.damageState.critical && + !user.hasActiveAbility?(:INFILTRATOR) + if target.pbOwnSide.effects[PBEffects::AuroraVeil] > 0 + if @battle.pbSideBattlerCount(target)>1 + multipliers[:final_damage_multiplier] *= 2 / 3.0 + else + multipliers[:final_damage_multiplier] /= 2 + end + elsif target.pbOwnSide.effects[PBEffects::Reflect] > 0 && physicalMove? + if @battle.pbSideBattlerCount(target)>1 + multipliers[:final_damage_multiplier] *= 2 / 3.0 + else + multipliers[:final_damage_multiplier] /= 2 + end + elsif target.pbOwnSide.effects[PBEffects::LightScreen] > 0 && specialMove? + if @battle.pbSideBattlerCount(target) > 1 + multipliers[:final_damage_multiplier] *= 2 / 3.0 + else + multipliers[:final_damage_multiplier] /= 2 + end + end + end + # Minimize + if target.effects[PBEffects::Minimize] && tramplesMinimize?(2) + multipliers[:final_damage_multiplier] *= 2 + end + # Move-specific base damage modifiers + multipliers[:base_damage_multiplier] = pbBaseDamageMultiplier(multipliers[:base_damage_multiplier], user, target) + # Move-specific final damage modifiers + multipliers[:final_damage_multiplier] = pbModifyDamage(multipliers[:final_damage_multiplier], user, target) + end + + #============================================================================= + # Additional effect chance + #============================================================================= + def pbAdditionalEffectChance(user,target,effectChance=0) + return 0 if target.hasActiveAbility?(:SHIELDDUST) && !@battle.moldBreaker + ret = (effectChance>0) ? effectChance : @addlEffect + if Settings::MECHANICS_GENERATION >= 6 || @function != "0A4" # Secret Power + ret *= 2 if user.hasActiveAbility?(:SERENEGRACE) || + user.pbOwnSide.effects[PBEffects::Rainbow]>0 + end + ret = 100 if $DEBUG && Input.press?(Input::CTRL) + return ret + end + + # NOTE: Flinching caused by a move's effect is applied in that move's code, + # not here. + def pbFlinchChance(user,target) + return 0 if flinchingMove? + return 0 if target.hasActiveAbility?(:SHIELDDUST) && !@battle.moldBreaker + ret = 0 + if user.hasActiveAbility?(:STENCH,true) + ret = 10 + elsif user.hasActiveItem?([:KINGSROCK,:RAZORFANG],true) + ret = 10 + end + ret *= 2 if user.hasActiveAbility?(:SERENEGRACE) || + user.pbOwnSide.effects[PBEffects::Rainbow]>0 + return ret + end +end diff --git a/Data/Scripts/011_Battle/002_Move/004_Move_Effects_Generic.rb b/Data/Scripts/011_Battle/002_Move/004_Move_Effects_Generic.rb new file mode 100644 index 000000000..63c4f21fd --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/004_Move_Effects_Generic.rb @@ -0,0 +1,716 @@ +#=============================================================================== +# Superclass that handles moves using a non-existent function code. +# Damaging moves just do damage with no additional effect. +# Status moves always fail. +#=============================================================================== +class PokeBattle_UnimplementedMove < PokeBattle_Move + def pbMoveFailed?(user,targets) + if statusMove? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + + + +#=============================================================================== +# Pseudomove for confusion damage. +#=============================================================================== +class PokeBattle_Confusion < PokeBattle_Move + def initialize(battle,move) + @battle = battle + @realMove = move + @id = 0 + @name = "" + @function = "000" + @baseDamage = 40 + @type = nil + @category = 0 + @accuracy = 100 + @pp = -1 + @target = 0 + @priority = 0 + @flags = "" + @addlEffect = 0 + @calcType = nil + @powerBoost = false + @snatched = false + end + + def physicalMove?(thisType=nil); return true; end + def specialMove?(thisType=nil); return false; end + def pbCritialOverride(user,target); return -1; end +end + + + +#=============================================================================== +# Implements the move Struggle. +# For cases where the real move named Struggle is not defined. +#=============================================================================== +class PokeBattle_Struggle < PokeBattle_Move + def initialize(battle,move) + @battle = battle + @realMove = nil # Not associated with a move + @id = (move) ? move.id : :STRUGGLE + @name = (move) ? move.name : _INTL("Struggle") + @function = "002" + @baseDamage = 50 + @type = nil + @category = 0 + @accuracy = 0 + @pp = -1 + @target = 0 + @priority = 0 + @flags = "" + @addlEffect = 0 + @calcType = nil + @powerBoost = false + @snatched = false + end + + def physicalMove?(thisType=nil); return true; end + def specialMove?(thisType=nil); return false; end + + def pbEffectAfterAllHits(user,target) + return if target.damageState.unaffected + user.pbReduceHP((user.totalhp/4.0).round,false) + @battle.pbDisplay(_INTL("{1} is damaged by recoil!",user.pbThis)) + user.pbItemHPHealCheck + end +end + + + +#=============================================================================== +# Generic status problem-inflicting classes. +#=============================================================================== +class PokeBattle_SleepMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanSleep?(user,true,self) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbSleep + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbSleep if target.pbCanSleep?(user,false,self) + end +end + + + +class PokeBattle_PoisonMove < PokeBattle_Move + def initialize(battle,move) + super + @toxic = false + end + + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanPoison?(user,true,self) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbPoison(user,nil,@toxic) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbPoison(user,nil,@toxic) if target.pbCanPoison?(user,false,self) + end +end + + + +class PokeBattle_ParalysisMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanParalyze?(user,true,self) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbParalyze(user) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbParalyze(user) if target.pbCanParalyze?(user,false,self) + end +end + + + +class PokeBattle_BurnMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanBurn?(user,true,self) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbBurn(user) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbBurn(user) if target.pbCanBurn?(user,false,self) + end +end + + + +class PokeBattle_FreezeMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanFreeze?(user,true,self) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbFreeze + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbFreeze if target.pbCanFreeze?(user,false,self) + end +end + + + +#=============================================================================== +# Other problem-causing classes. +#=============================================================================== +class PokeBattle_FlinchMove < PokeBattle_Move + def flinchingMove?; return true; end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbFlinch(user) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbFlinch(user) + end +end + + + +class PokeBattle_ConfuseMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanConfuse?(user,true,self) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbConfuse + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + return if !target.pbCanConfuse?(user,false,self) + target.pbConfuse + end +end + + + +#=============================================================================== +# Generic user's stat increase/decrease classes. +#=============================================================================== +class PokeBattle_StatUpMove < PokeBattle_Move + def pbMoveFailed?(user,targets) + return false if damagingMove? + return !user.pbCanRaiseStatStage?(@statUp[0],user,self,true) + end + + def pbEffectGeneral(user) + return if damagingMove? + user.pbRaiseStatStage(@statUp[0],@statUp[1],user) + end + + def pbAdditionalEffect(user,target) + if user.pbCanRaiseStatStage?(@statUp[0],user,self) + user.pbRaiseStatStage(@statUp[0],@statUp[1],user) + end + end +end + + + +class PokeBattle_MultiStatUpMove < PokeBattle_Move + def pbMoveFailed?(user,targets) + return false if damagingMove? + failed = true + for i in 0...@statUp.length/2 + next if !user.pbCanRaiseStatStage?(@statUp[i*2],user,self) + failed = false + break + end + if failed + @battle.pbDisplay(_INTL("{1}'s stats won't go any higher!",user.pbThis)) + return true + end + return false + end + + def pbEffectGeneral(user) + return if damagingMove? + showAnim = true + for i in 0...@statUp.length/2 + next if !user.pbCanRaiseStatStage?(@statUp[i*2],user,self) + if user.pbRaiseStatStage(@statUp[i*2],@statUp[i*2+1],user,showAnim) + showAnim = false + end + end + end + + def pbAdditionalEffect(user,target) + showAnim = true + for i in 0...@statUp.length/2 + next if !user.pbCanRaiseStatStage?(@statUp[i*2],user,self) + if user.pbRaiseStatStage(@statUp[i*2],@statUp[i*2+1],user,showAnim) + showAnim = false + end + end + end +end + + + +class PokeBattle_StatDownMove < PokeBattle_Move + def pbEffectWhenDealingDamage(user,target) + return if @battle.pbAllFainted?(target.idxOwnSide) + showAnim = true + for i in 0...@statDown.length/2 + next if !user.pbCanLowerStatStage?(@statDown[i*2],user,self) + if user.pbLowerStatStage(@statDown[i*2],@statDown[i*2+1],user,showAnim) + showAnim = false + end + end + end +end + + + +#=============================================================================== +# Generic target's stat increase/decrease classes. +#=============================================================================== +class PokeBattle_TargetStatDownMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + return !target.pbCanLowerStatStage?(@statDown[0],user,self,true) + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.pbLowerStatStage(@statDown[0],@statDown[1],user) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + return if !target.pbCanLowerStatStage?(@statDown[0],user,self) + target.pbLowerStatStage(@statDown[0],@statDown[1],user) + end +end + + + +class PokeBattle_TargetMultiStatDownMove < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + failed = true + for i in 0...@statDown.length/2 + next if !target.pbCanLowerStatStage?(@statDown[i*2],user,self) + failed = false + break + end + if failed + # NOTE: It's a bit of a faff to make sure the appropriate failure message + # is shown here, I know. + canLower = false + if target.hasActiveAbility?(:CONTRARY) && !@battle.moldBreaker + for i in 0...@statDown.length/2 + next if target.statStageAtMax?(@statDown[i*2]) + canLower = true + break + end + @battle.pbDisplay(_INTL("{1}'s stats won't go any higher!",user.pbThis)) if !canLower + else + for i in 0...@statDown.length/2 + next if target.statStageAtMin?(@statDown[i*2]) + canLower = true + break + end + @battle.pbDisplay(_INTL("{1}'s stats won't go any lower!",user.pbThis)) if !canLower + end + if canLower + target.pbCanLowerStatStage?(@statDown[0],user,self,true) + end + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + showAnim = true + for i in 0...@statDown.length/2 + next if !target.pbCanLowerStatStage?(@statDown[i*2],user,self) + if target.pbLowerStatStage(@statDown[i*2],@statDown[i*2+1],user,showAnim) + showAnim = false + end + end + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + showAnim = true + for i in 0...@statDown.length/2 + next if !target.pbCanLowerStatStage?(@statDown[i*2],user,self) + if target.pbLowerStatStage(@statDown[i*2],@statDown[i*2+1],user,showAnim) + showAnim = false + end + end + end +end + + + +#=============================================================================== +# Fixed damage-inflicting move. +#=============================================================================== +class PokeBattle_FixedDamageMove < PokeBattle_Move + def pbFixedDamage(user,target); return 1; end + + def pbCalcDamage(user,target,numTargets=1) + target.damageState.critical = false + target.damageState.calcDamage = pbFixedDamage(user,target) + target.damageState.calcDamage = 1 if target.damageState.calcDamage<1 + end +end + + + +#=============================================================================== +# Two turn move. +#=============================================================================== +class PokeBattle_TwoTurnMove < PokeBattle_Move + def chargingTurnMove?; return true; end + + # user.effects[PBEffects::TwoTurnAttack] is set to the move's ID if this + # method returns true, or nil if false. + # Non-nil means the charging turn. nil means the attacking turn. + def pbIsChargingTurn?(user) + @powerHerb = false + @chargingTurn = false # Assume damaging turn by default + @damagingTurn = true + # 0 at start of charging turn, move's ID at start of damaging turn + if !user.effects[PBEffects::TwoTurnAttack] + @powerHerb = user.hasActiveItem?(:POWERHERB) + @chargingTurn = true + @damagingTurn = @powerHerb + end + return !@damagingTurn # Deliberately not "return @chargingTurn" + end + + def pbDamagingMove? # Stops damage being dealt in the first (charging) turn + return false if !@damagingTurn + return super + end + + def pbAccuracyCheck(user,target) + return true if !@damagingTurn + return super + end + + def pbInitialEffect(user,targets,hitNum) + pbChargingTurnMessage(user,targets) if @chargingTurn + if @chargingTurn && @damagingTurn # Move only takes one turn to use + pbShowAnimation(@id,user,targets,1) # Charging anim + targets.each { |b| pbChargingTurnEffect(user,b) } + if @powerHerb + # Moves that would make the user semi-invulnerable will hide the user + # after the charging animation, so the "UseItem" animation shouldn't show + # for it + if !["0C9","0CA","0CB","0CC","0CD","0CE","14D"].include?(@function) + @battle.pbCommonAnimation("UseItem",user) + end + @battle.pbDisplay(_INTL("{1} became fully charged due to its Power Herb!",user.pbThis)) + user.pbConsumeItem + end + end + pbAttackingTurnMessage(user,targets) if @damagingTurn + end + + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} began charging up!",user.pbThis)) + end + + def pbAttackingTurnMessage(user,targets) + end + + def pbChargingTurnEffect(user,target) + # Skull Bash/Sky Drop are the only two-turn moves with an effect here, and + # the latter just records the target is being Sky Dropped + end + + def pbAttackingTurnEffect(user,target) + end + + def pbEffectAgainstTarget(user,target) + if @damagingTurn; pbAttackingTurnEffect(user,target) + elsif @chargingTurn; pbChargingTurnEffect(user,target) + end + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + hitNum = 1 if @chargingTurn && !@damagingTurn # Charging anim + super + end +end + + + +#=============================================================================== +# Healing move. +#=============================================================================== +class PokeBattle_HealingMove < PokeBattle_Move + def healingMove?; return true; end + def pbHealAmount(user); return 1; end + + def pbMoveFailed?(user,targets) + if user.hp==user.totalhp + @battle.pbDisplay(_INTL("{1}'s HP is full!",user.pbThis)) + return true + end + return false + end + + def pbEffectGeneral(user) + amt = pbHealAmount(user) + user.pbRecoverHP(amt) + @battle.pbDisplay(_INTL("{1}'s HP was restored.",user.pbThis)) + end +end + + + +#=============================================================================== +# Recoil move. +#=============================================================================== +class PokeBattle_RecoilMove < PokeBattle_Move + def recoilMove?; return true; end + def pbRecoilDamage(user,target); return 1; end + + def pbEffectAfterAllHits(user,target) + return if target.damageState.unaffected + return if !user.takesIndirectDamage? + return if user.hasActiveAbility?(:ROCKHEAD) + amt = pbRecoilDamage(user,target) + amt = 1 if amt<1 + user.pbReduceHP(amt,false) + @battle.pbDisplay(_INTL("{1} is damaged by recoil!",user.pbThis)) + user.pbItemHPHealCheck + end +end + + + +#=============================================================================== +# Protect move. +#=============================================================================== +class PokeBattle_ProtectMove < PokeBattle_Move + def initialize(battle,move) + super + @sidedEffect = false + end + + def pbChangeUsageCounters(user,specialUsage) + oldVal = user.effects[PBEffects::ProtectRate] + super + user.effects[PBEffects::ProtectRate] = oldVal + end + + def pbMoveFailed?(user,targets) + if @sidedEffect + if user.pbOwnSide.effects[@effect] + user.effects[PBEffects::ProtectRate] = 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + elsif user.effects[@effect] + user.effects[PBEffects::ProtectRate] = 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if (!@sidedEffect || Settings::MECHANICS_GENERATION <= 5) && + user.effects[PBEffects::ProtectRate]>1 && + @battle.pbRandom(user.effects[PBEffects::ProtectRate])!=0 + user.effects[PBEffects::ProtectRate] = 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if pbMoveFailedLastInRound?(user) + user.effects[PBEffects::ProtectRate] = 1 + return true + end + return false + end + + def pbEffectGeneral(user) + if @sidedEffect + user.pbOwnSide.effects[@effect] = true + else + user.effects[@effect] = true + end + user.effects[PBEffects::ProtectRate] *= (Settings::MECHANICS_GENERATION >= 6) ? 3 : 2 + pbProtectMessage(user) + end + + def pbProtectMessage(user) + if @sidedEffect + @battle.pbDisplay(_INTL("{1} protected {2}!",@name,user.pbTeam(true))) + else + @battle.pbDisplay(_INTL("{1} protected itself!",user.pbThis)) + end + end +end + + + +#=============================================================================== +# Weather-inducing move. +#=============================================================================== +class PokeBattle_WeatherMove < PokeBattle_Move + def initialize(battle,move) + super + @weatherType = :None + end + + def pbMoveFailed?(user,targets) + case @battle.field.weather + when :HarshSun + @battle.pbDisplay(_INTL("The extremely harsh sunlight was not lessened at all!")) + return true + when :HeavyRain + @battle.pbDisplay(_INTL("There is no relief from this heavy rain!")) + return true + when :StrongWinds + @battle.pbDisplay(_INTL("The mysterious air current blows on regardless!")) + return true + when @weatherType + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.pbStartWeather(user,@weatherType,true,false) + end +end + + + +#=============================================================================== +# Pledge move. +#=============================================================================== +class PokeBattle_PledgeMove < PokeBattle_Move + def pbOnStartUse(user,targets) + @pledgeSetup = false; @pledgeCombo = false; @pledgeOtherUser = nil + @comboEffect = nil; @overrideType = nil; @overrideAnim = nil + # Check whether this is the use of a combo move + @combos.each do |i| + next if i[0]!=user.effects[PBEffects::FirstPledge] + @battle.pbDisplay(_INTL("The two moves have become one! It's a combined move!")) + @pledgeCombo = true + @comboEffect = i[1]; @overrideType = i[2]; @overrideAnim = i[3] + @overrideType = nil if !GameData::Type.exists?(@overrideType) + break + end + return if @pledgeCombo + # Check whether this is the setup of a combo move + user.eachAlly do |b| + next if @battle.choices[b.index][0]!=:UseMove || b.movedThisRound? + move = @battle.choices[b.index][2] + next if !move + @combos.each do |i| + next if i[0]!=move.function + @pledgeSetup = true + @pledgeOtherUser = b + break + end + break if @pledgeSetup + end + end + + def pbDamagingMove? + return false if @pledgeSetup + return super + end + + def pbBaseType(user) + return @overrideType if @overrideType!=nil + return super + end + + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if @pledgeCombo + return baseDmg + end + + def pbEffectGeneral(user) + user.effects[PBEffects::FirstPledge] = 0 + return if !@pledgeSetup + @battle.pbDisplay(_INTL("{1} is waiting for {2}'s move...", + user.pbThis,@pledgeOtherUser.pbThis(true))) + @pledgeOtherUser.effects[PBEffects::FirstPledge] = @function + @pledgeOtherUser.effects[PBEffects::MoveNext] = true + user.lastMoveFailed = true # Treated as a failure for Stomping Tantrum + end + + def pbEffectAfterAllHits(user,target) + return if !@pledgeCombo + msg = nil; animName = nil + case @comboEffect + when :SeaOfFire # Grass + Fire + if user.pbOpposingSide.effects[PBEffects::SeaOfFire]==0 + user.pbOpposingSide.effects[PBEffects::SeaOfFire] = 4 + msg = _INTL("A sea of fire enveloped {1}!",user.pbOpposingTeam(true)) + animName = (user.opposes?) ? "SeaOfFire" : "SeaOfFireOpp" + end + when :Rainbow # Fire + Water + if user.pbOwnSide.effects[PBEffects::Rainbow]==0 + user.pbOwnSide.effects[PBEffects::Rainbow] = 4 + msg = _INTL("A rainbow appeared in the sky on {1}'s side!",user.pbTeam(true)) + animName = (user.opposes?) ? "RainbowOpp" : "Rainbow" + end + when :Swamp # Water + Grass + if user.pbOpposingSide.effects[PBEffects::Swamp]==0 + user.pbOpposingSide.effects[PBEffects::Swamp] = 4 + msg = _INTL("A swamp enveloped {1}!",user.pbOpposingTeam(true)) + animName = (user.opposes?) ? "Swamp" : "SwampOpp" + end + end + @battle.pbDisplay(msg) if msg + @battle.pbCommonAnimation(animName) if animName + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + return if @pledgeSetup # No animation for setting up + id = @overrideAnim if @overrideAnim + return super + end +end diff --git a/Data/Scripts/011_Battle/002_Move/005_Move_Effects_000-07F.rb b/Data/Scripts/011_Battle/002_Move/005_Move_Effects_000-07F.rb new file mode 100644 index 000000000..ed6194b61 --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/005_Move_Effects_000-07F.rb @@ -0,0 +1,2572 @@ +#=============================================================================== +# No additional effect. +#=============================================================================== +class PokeBattle_Move_000 < PokeBattle_Move +end + +#=============================================================================== +# Does absolutely nothing. (Splash) +#=============================================================================== +class PokeBattle_Move_001 < PokeBattle_Move + def unusableInGravity? + return true; + end + + def pbEffectGeneral(user) + @battle.pbDisplay(_INTL("But nothing happened!")) + end +end + +#=============================================================================== +# Struggle, if defined as a move in moves.txt. Typically it won't be. +#=============================================================================== +class PokeBattle_Move_002 < PokeBattle_Struggle +end + +#=============================================================================== +# Puts the target to sleep. +#=============================================================================== +class PokeBattle_Move_003 < PokeBattle_SleepMove + def pbMoveFailed?(user, targets) + if Settings::MECHANICS_GENERATION >= 7 && @id == :DARKVOID + if !user.isSpecies?(:DARKRAI) && user.effects[PBEffects::TransformSpecies] != :DARKRAI + @battle.pbDisplay(_INTL("But {1} can't use the move!", user.pbThis)) + return true + end + end + return false + end + + def pbEndOfMoveUsageEffect(user, targets, numHits, switchedBattlers) + return if numHits == 0 + return if user.fainted? || user.effects[PBEffects::Transform] + return if @id != :RELICSONG + return if !(user.isFusionOf(:MELOETTA_A) || user.isFusionOf(:MELOETTA_P)) + return if user.hasActiveAbility?(:SHEERFORCE) && @addlEffect > 0 + + is_meloetta_A = user.isFusionOf(:MELOETTA_A) + is_meloetta_P = user.isFusionOf(:MELOETTA_P) + #reverse the fusion if it's a meloA and meloP fusion + # There's probably a smarter way to do this but laziness lol + if is_meloetta_A && is_meloetta_P + body_id = user.pokemon.species_data.get_body_species() + body_species = GameData::Species.get(body_id) + if body_species == :MELOETTA_A + changeSpeciesSpecific(user.pokemon,:B467H466) + else + changeSpeciesSpecific(user.pokemon,:B466H467) + end + user.playChangeFormAnimation("Shiny") + else + user.changeSpecies(user.pokemon, :MELOETTA_A, :MELOETTA_P, "Shiny") if is_meloetta_A + user.changeSpecies(user.pokemon, :MELOETTA_P, :MELOETTA_A, "Shiny") if is_meloetta_P + end + end +end + +#=============================================================================== +# Makes the target drowsy; it falls asleep at the end of the next turn. (Yawn) +#=============================================================================== +class PokeBattle_Move_004 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if target.effects[PBEffects::Yawn] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if !target.pbCanSleep?(user, true, self) + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Yawn] = 2 + @battle.pbDisplay(_INTL("{1} made {2} drowsy!", user.pbThis, target.pbThis(true))) + end +end + +#=============================================================================== +# Poisons the target. +#=============================================================================== +class PokeBattle_Move_005 < PokeBattle_PoisonMove +end + +#=============================================================================== +# Badly poisons the target. (Poison Fang, Toxic) +#=============================================================================== +class PokeBattle_Move_006 < PokeBattle_PoisonMove + def initialize(battle, move) + super + @toxic = true + end + + def pbOverrideSuccessCheckPerHit(user, target) + return (Settings::MORE_TYPE_EFFECTS && statusMove? && user.pbHasType?(:POISON)) + end +end + +#=============================================================================== +# Paralyzes the target. +# Thunder Wave: Doesn't affect target if move's type has no effect on it. +# Body Slam: Does double damage and has perfect accuracy if target is Minimized. +#=============================================================================== +class PokeBattle_Move_007 < PokeBattle_ParalysisMove + def tramplesMinimize?(param = 1) + # Perfect accuracy and double damage (for Body Slam only) + return Settings::MECHANICS_GENERATION >= 6 if @id == :BODYSLAM + return super + end + + def pbFailsAgainstTarget?(user, target) + if @id == :THUNDERWAVE && Effectiveness.ineffective?(target.damageState.typeMod) + @battle.pbDisplay(_INTL("It doesn't affect {1}...", target.pbThis(true))) + return true + end + return super + end +end + +#=============================================================================== +# Paralyzes the target. Accuracy perfect in rain, 50% in sunshine. Hits some +# semi-invulnerable targets. (Thunder) +#=============================================================================== +class PokeBattle_Move_008 < PokeBattle_ParalysisMove + def hitsFlyingTargets? + return true; + end + + def pbBaseAccuracy(user, target) + case @battle.pbWeather + when :Sun, :HarshSun + return 50 + when :Rain, :HeavyRain + return 0 + end + return super + end +end + +#=============================================================================== +# Paralyzes the target. May cause the target to flinch. (Thunder Fang) +#=============================================================================== +class PokeBattle_Move_009 < PokeBattle_Move + def flinchingMove? + return true; + end + + def pbAdditionalEffect(user, target) + return if target.damageState.substitute + chance = pbAdditionalEffectChance(user, target, 10) + return if chance == 0 + if @battle.pbRandom(100) < chance + target.pbParalyze(user) if target.pbCanParalyze?(user, false, self) + end + target.pbFlinch(user) if @battle.pbRandom(100) < chance + end +end + +#=============================================================================== +# Burns the target. +#=============================================================================== +class PokeBattle_Move_00A < PokeBattle_BurnMove +end + +#=============================================================================== +# Burns the target. May cause the target to flinch. (Fire Fang) +#=============================================================================== +class PokeBattle_Move_00B < PokeBattle_Move + def flinchingMove? + return true; + end + + def pbAdditionalEffect(user, target) + return if target.damageState.substitute + chance = pbAdditionalEffectChance(user, target, 10) + return if chance == 0 + if @battle.pbRandom(100) < chance + target.pbBurn(user) if target.pbCanBurn?(user, false, self) + end + target.pbFlinch(user) if @battle.pbRandom(100) < chance + end +end + +#=============================================================================== +# Freezes the target. +#=============================================================================== +class PokeBattle_Move_00C < PokeBattle_FreezeMove +end + +#=============================================================================== +# Freezes the target. Accuracy perfect in hail. (Blizzard) +#=============================================================================== +class PokeBattle_Move_00D < PokeBattle_FreezeMove + def pbBaseAccuracy(user, target) + return 0 if @battle.pbWeather == :Hail + return super + end +end + +#=============================================================================== +# Freezes the target. May cause the target to flinch. (Ice Fang) +#=============================================================================== +class PokeBattle_Move_00E < PokeBattle_Move + def flinchingMove? + return true; + end + + def pbAdditionalEffect(user, target) + return if target.damageState.substitute + chance = pbAdditionalEffectChance(user, target, 10) + return if chance == 0 + if @battle.pbRandom(100) < chance + target.pbFreeze if target.pbCanFreeze?(user, false, self) + end + target.pbFlinch(user) if @battle.pbRandom(100) < chance + end +end + +#=============================================================================== +# Causes the target to flinch. +#=============================================================================== +class PokeBattle_Move_00F < PokeBattle_FlinchMove +end + +#=============================================================================== +# Causes the target to flinch. Does double damage and has perfect accuracy if +# the target is Minimized. (Dragon Rush, Steamroller, Stomp) +#=============================================================================== +class PokeBattle_Move_010 < PokeBattle_FlinchMove + def tramplesMinimize?(param = 1) + return super if @id == :DRAGONRUSH && Settings::MECHANICS_GENERATION <= 5 + return true if param == 1 && Settings::MECHANICS_GENERATION >= 6 # Perfect accuracy + return true if param == 2 # Double damage + return super + end +end + +#=============================================================================== +# Causes the target to flinch. Fails if the user is not asleep. (Snore) +#=============================================================================== +class PokeBattle_Move_011 < PokeBattle_FlinchMove + def usableWhenAsleep? + return true; + end + + def pbMoveFailed?(user, targets) + if !user.asleep? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# Causes the target to flinch. Fails if this isn't the user's first turn. +# (Fake Out) +#=============================================================================== +class PokeBattle_Move_012 < PokeBattle_FlinchMove + def pbMoveFailed?(user, targets) + if user.turnCount > 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# Confuses the target. +#=============================================================================== +class PokeBattle_Move_013 < PokeBattle_ConfuseMove +end + +#=============================================================================== +# Confuses the target. (Chatter) +#=============================================================================== +class PokeBattle_Move_014 < PokeBattle_Move_013 +end + +#=============================================================================== +# Confuses the target. Accuracy perfect in rain, 50% in sunshine. Hits some +# semi-invulnerable targets. (Hurricane) +#=============================================================================== +class PokeBattle_Move_015 < PokeBattle_ConfuseMove + def hitsFlyingTargets? + return true; + end + + def pbBaseAccuracy(user, target) + case @battle.pbWeather + when :Sun, :HarshSun + return 50 + when :Rain, :HeavyRain + return 0 + end + return super + end +end + +#=============================================================================== +# Attracts the target. (Attract) +#=============================================================================== +class PokeBattle_Move_016 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbFailsAgainstTarget?(user, target) + return false if damagingMove? + return true if !target.pbCanAttract?(user) + return true if pbMoveFailedAromaVeil?(user, target) + return false + end + + def pbEffectAgainstTarget(user, target) + return if damagingMove? + target.pbAttract(user) + end + + def pbAdditionalEffect(user, target) + return if target.damageState.substitute + target.pbAttract(user) if target.pbCanAttract?(user, false) + end +end + +#=============================================================================== +# Burns, freezes or paralyzes the target. (Tri Attack) +#=============================================================================== +class PokeBattle_Move_017 < PokeBattle_Move + def pbAdditionalEffect(user, target) + return if target.damageState.substitute + case @battle.pbRandom(3) + when 0 then + target.pbBurn(user) if target.pbCanBurn?(user, false, self) + when 1 then + target.pbFreeze if target.pbCanFreeze?(user, false, self) + when 2 then + target.pbParalyze(user) if target.pbCanParalyze?(user, false, self) + end + end +end + +#=============================================================================== +# Cures user of burn, poison and paralysis. (Refresh) +#=============================================================================== +class PokeBattle_Move_018 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if ![:BURN, :POISON, :PARALYSIS].include?(user.status) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + old_status = user.status + user.pbCureStatus(false) + case old_status + when :BURN + @battle.pbDisplay(_INTL("{1} healed its burn!", user.pbThis)) + when :POISON + @battle.pbDisplay(_INTL("{1} cured its poisoning!", user.pbThis)) + when :PARALYSIS + @battle.pbDisplay(_INTL("{1} cured its paralysis!", user.pbThis)) + end + end +end + +#=============================================================================== +# Cures all party Pokémon of permanent status problems. (Aromatherapy, Heal Bell) +#=============================================================================== +# NOTE: In Gen 5, this move should have a target of UserSide, while in Gen 6+ it +# should have a target of UserAndAllies. This is because, in Gen 5, this +# move shouldn't call def pbSuccessCheckAgainstTarget for each Pokémon +# currently in battle that will be affected by this move (i.e. allies +# aren't protected by their substitute/ability/etc., but they are in Gen +# 6+). We achieve this by not targeting any battlers in Gen 5, since +# pbSuccessCheckAgainstTarget is only called for targeted battlers. +class PokeBattle_Move_019 < PokeBattle_Move + def worksWithNoTargets? + return true; + end + + def pbMoveFailed?(user, targets) + failed = true + @battle.eachSameSideBattler(user) do |b| + next if b.status == :NONE + failed = false + break + end + @battle.pbParty(user.index).each do |pkmn| + next if !pkmn || !pkmn.able? || pkmn.status == :NONE + failed = false + break + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + return target.status == :NONE + end + + def pbAromatherapyHeal(pkmn, battler = nil) + oldStatus = (battler) ? battler.status : pkmn.status + curedName = (battler) ? battler.pbThis : pkmn.name + if battler + battler.pbCureStatus(false) + else + pkmn.status = :NONE + pkmn.statusCount = 0 + end + case oldStatus + when :SLEEP + @battle.pbDisplay(_INTL("{1} was woken from sleep.", curedName)) + when :POISON + @battle.pbDisplay(_INTL("{1} was cured of its poisoning.", curedName)) + when :BURN + @battle.pbDisplay(_INTL("{1}'s burn was healed.", curedName)) + when :PARALYSIS + @battle.pbDisplay(_INTL("{1} was cured of paralysis.", curedName)) + when :FROZEN + @battle.pbDisplay(_INTL("{1} was thawed out.", curedName)) + end + end + + def pbEffectAgainstTarget(user, target) + # Cure all Pokémon in battle on the user's side. + pbAromatherapyHeal(target.pokemon, target) + end + + def pbEffectGeneral(user) + # Cure all Pokémon in battle on the user's side. For the benefit of the Gen + # 5 version of this move, to make Pokémon out in battle get cured first. + if pbTarget(user) == :UserSide + @battle.eachSameSideBattler(user) do |b| + next if b.status == :NONE + pbAromatherapyHeal(b.pokemon, b) + end + end + # Cure all Pokémon in the user's and partner trainer's party. + # NOTE: This intentionally affects the partner trainer's inactive Pokémon + # too. + @battle.pbParty(user.index).each_with_index do |pkmn, i| + next if !pkmn || !pkmn.able? || pkmn.status == :NONE + next if @battle.pbFindBattler(i, user) # Skip Pokémon in battle + pbAromatherapyHeal(pkmn) + end + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + super + if @id == :AROMATHERAPY + @battle.pbDisplay(_INTL("A soothing aroma wafted through the area!")) + elsif @id == :HEALBELL + @battle.pbDisplay(_INTL("A bell chimed!")) + end + end +end + +#=============================================================================== +# Safeguards the user's side from being inflicted with status problems. +# (Safeguard) +#=============================================================================== +class PokeBattle_Move_01A < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOwnSide.effects[PBEffects::Safeguard] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::Safeguard] = 5 + @battle.pbDisplay(_INTL("{1} became cloaked in a mystical veil!", user.pbTeam)) + end +end + +#=============================================================================== +# User passes its status problem to the target. (Psycho Shift) +#=============================================================================== +class PokeBattle_Move_01B < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.status == :NONE + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if !target.pbCanInflictStatus?(user.status, user, false, self) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + msg = "" + case user.status + when :SLEEP + target.pbSleep + msg = _INTL("{1} woke up.", user.pbThis) + when :POISON + target.pbPoison(user, nil, user.statusCount != 0) + msg = _INTL("{1} was cured of its poisoning.", user.pbThis) + when :BURN + target.pbBurn(user) + msg = _INTL("{1}'s burn was healed.", user.pbThis) + when :PARALYSIS + target.pbParalyze(user) + msg = _INTL("{1} was cured of paralysis.", user.pbThis) + when :FROZEN + target.pbFreeze + msg = _INTL("{1} was thawed out.", user.pbThis) + end + if msg != "" + user.pbCureStatus(false) + @battle.pbDisplay(msg) + end + end +end + +#=============================================================================== +# Increases the user's Attack by 1 stage. +#=============================================================================== +class PokeBattle_Move_01C < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1] + end +end + +#=============================================================================== +# Increases the user's Defense by 1 stage. (Harden, Steel Wing, Withdraw) +#=============================================================================== +class PokeBattle_Move_01D < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:DEFENSE, 1] + end +end + +#=============================================================================== +# Increases the user's Defense by 1 stage. User curls up. (Defense Curl) +#=============================================================================== +class PokeBattle_Move_01E < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:DEFENSE, 1] + end + + def pbEffectGeneral(user) + user.effects[PBEffects::DefenseCurl] = true + super + end +end + +#=============================================================================== +# Increases the user's Speed by 1 stage. (Flame Charge) +#=============================================================================== +class PokeBattle_Move_01F < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPEED, 1] + end +end + +#=============================================================================== +# Increases the user's Special Attack by 1 stage. (Charge Beam, Fiery Dance) +#=============================================================================== +class PokeBattle_Move_020 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_ATTACK, 1] + end +end + +#=============================================================================== +# Increases the user's Special Defense by 1 stage. +# Charges up user's next attack if it is Electric-type. (Charge) +#=============================================================================== +class PokeBattle_Move_021 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_DEFENSE, 1] + end + + def pbEffectGeneral(user) + user.effects[PBEffects::Charge] = 2 + @battle.pbDisplay(_INTL("{1} began charging power!", user.pbThis)) + super + end +end + +#=============================================================================== +# Increases the user's evasion by 1 stage. (Double Team) +#=============================================================================== +class PokeBattle_Move_022 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:EVASION, 1] + end +end + +#=============================================================================== +# Increases the user's critical hit rate. (Focus Energy) +#=============================================================================== +class PokeBattle_Move_023 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::FocusEnergy] >= 2 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::FocusEnergy] = 2 + @battle.pbDisplay(_INTL("{1} is getting pumped!", user.pbThis)) + end +end + +#=============================================================================== +# Increases the user's Attack and Defense by 1 stage each. (Bulk Up) +#=============================================================================== +class PokeBattle_Move_024 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :DEFENSE, 1] + end +end + +#=============================================================================== +# Increases the user's Attack, Defense and accuracy by 1 stage each. (Coil) +#=============================================================================== +class PokeBattle_Move_025 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :DEFENSE, 1, :ACCURACY, 1] + end +end + +#=============================================================================== +# Increases the user's Attack and Speed by 1 stage each. (Dragon Dance) +#=============================================================================== +class PokeBattle_Move_026 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :SPEED, 1] + end +end + +#=============================================================================== +# Increases the user's Attack and Special Attack by 1 stage each. (Work Up) +#=============================================================================== +class PokeBattle_Move_027 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :SPECIAL_ATTACK, 1] + end +end + +#=============================================================================== +# Increases the user's Attack and Sp. Attack by 1 stage each. +# In sunny weather, increases are 2 stages each instead. (Growth) +#=============================================================================== +class PokeBattle_Move_028 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :SPECIAL_ATTACK, 1] + end + + def pbOnStartUse(user, targets) + increment = 1 + increment = 2 if [:Sun, :HarshSun].include?(@battle.pbWeather) + @statUp[1] = @statUp[3] = increment + end +end + +#=============================================================================== +# Increases the user's Attack and accuracy by 1 stage each. (Hone Claws) +#=============================================================================== +class PokeBattle_Move_029 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :ACCURACY, 1] + end +end + +#=============================================================================== +# Increases the user's Defense and Special Defense by 1 stage each. +# (Cosmic Power, Defend Order) +#=============================================================================== +class PokeBattle_Move_02A < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:DEFENSE, 1, :SPECIAL_DEFENSE, 1] + end +end + +#=============================================================================== +# Increases the user's Sp. Attack, Sp. Defense and Speed by 1 stage each. +# (Quiver Dance) +#=============================================================================== +class PokeBattle_Move_02B < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_ATTACK, 1, :SPECIAL_DEFENSE, 1, :SPEED, 1] + end +end + +#=============================================================================== +# Increases the user's Sp. Attack and Sp. Defense by 1 stage each. (Calm Mind) +#=============================================================================== +class PokeBattle_Move_02C < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_ATTACK, 1, :SPECIAL_DEFENSE, 1] + end +end + +#=============================================================================== +# Increases the user's Attack, Defense, Speed, Special Attack and Special Defense +# by 1 stage each. (Ancient Power, Ominous Wind, Silver Wind) +#=============================================================================== +class PokeBattle_Move_02D < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 1, :DEFENSE, 1, :SPECIAL_ATTACK, 1, :SPECIAL_DEFENSE, 1, :SPEED, 1] + end +end + +#=============================================================================== +# Increases the user's Attack by 2 stages. (Swords Dance) +#=============================================================================== +class PokeBattle_Move_02E < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:ATTACK, 2] + end +end + +#=============================================================================== +# Increases the user's Defense by 2 stages. (Acid Armor, Barrier, Iron Defense) +#=============================================================================== +class PokeBattle_Move_02F < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:DEFENSE, 2] + end +end + +#=============================================================================== +# Increases the user's Speed by 2 stages. (Agility, Rock Polish) +#=============================================================================== +class PokeBattle_Move_030 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPEED, 2] + end +end + +#=============================================================================== +# Increases the user's Speed by 2 stages. Lowers user's weight by 100kg. +# (Autotomize) +#=============================================================================== +class PokeBattle_Move_031 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPEED, 2] + end + + def pbEffectGeneral(user) + if user.pbWeight + user.effects[PBEffects::WeightChange] > 1 + user.effects[PBEffects::WeightChange] -= 1000 + @battle.pbDisplay(_INTL("{1} became nimble!", user.pbThis)) + end + super + end +end + +#=============================================================================== +# Increases the user's Special Attack by 2 stages. (Nasty Plot) +#=============================================================================== +class PokeBattle_Move_032 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_ATTACK, 2] + end +end + +#=============================================================================== +# Increases the user's Special Defense by 2 stages. (Amnesia) +#=============================================================================== +class PokeBattle_Move_033 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_DEFENSE, 2] + end +end + +#=============================================================================== +# Increases the user's evasion by 2 stages. Minimizes the user. (Minimize) +#=============================================================================== +class PokeBattle_Move_034 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:EVASION, 2] + end + + def pbEffectGeneral(user) + user.effects[PBEffects::Minimize] = true + super + end +end + +#=============================================================================== +# Decreases the user's Defense and Special Defense by 1 stage each. +# Increases the user's Attack, Speed and Special Attack by 2 stages each. +# (Shell Smash) +#=============================================================================== +class PokeBattle_Move_035 < PokeBattle_Move + def initialize(battle, move) + super + @statUp = [:ATTACK, 2, :SPECIAL_ATTACK, 2, :SPEED, 2] + @statDown = [:DEFENSE, 1, :SPECIAL_DEFENSE, 1] + end + + def pbMoveFailed?(user, targets) + failed = true + for i in 0...@statUp.length / 2 + if user.pbCanRaiseStatStage?(@statUp[i * 2], user, self) + failed = false; break + end + end + for i in 0...@statDown.length / 2 + if user.pbCanLowerStatStage?(@statDown[i * 2], user, self) + failed = false; break + end + end + if failed + @battle.pbDisplay(_INTL("{1}'s stats can't be changed further!", user.pbThis)) + return true + end + return false + end + + def pbEffectGeneral(user) + showAnim = true + for i in 0...@statDown.length / 2 + next if !user.pbCanLowerStatStage?(@statDown[i * 2], user, self) + if user.pbLowerStatStage(@statDown[i * 2], @statDown[i * 2 + 1], user, showAnim) + showAnim = false + end + end + showAnim = true + for i in 0...@statUp.length / 2 + next if !user.pbCanRaiseStatStage?(@statUp[i * 2], user, self) + if user.pbRaiseStatStage(@statUp[i * 2], @statUp[i * 2 + 1], user, showAnim) + showAnim = false + end + end + end +end + +#=============================================================================== +# Increases the user's Speed by 2 stages, and its Attack by 1 stage. (Shift Gear) +#=============================================================================== +class PokeBattle_Move_036 < PokeBattle_MultiStatUpMove + def initialize(battle, move) + super + @statUp = [:SPEED, 2, :ATTACK, 1] + end +end + +#=============================================================================== +# Increases one random stat of the target by 2 stages (except HP). (Acupressure) +#=============================================================================== +class PokeBattle_Move_037 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + @statArray = [] + GameData::Stat.each_battle do |s| + @statArray.push(s.id) if target.pbCanRaiseStatStage?(s.id, user, self) + end + if @statArray.length == 0 + @battle.pbDisplay(_INTL("{1}'s stats won't go any higher!", target.pbThis)) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + stat = @statArray[@battle.pbRandom(@statArray.length)] + target.pbRaiseStatStage(stat, 2, user) + end +end + +#=============================================================================== +# Increases the user's Defense by 3 stages. (Cotton Guard) +#=============================================================================== +class PokeBattle_Move_038 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:DEFENSE, 3] + end +end + +#=============================================================================== +# Increases the user's Special Attack by 3 stages. (Tail Glow) +#=============================================================================== +class PokeBattle_Move_039 < PokeBattle_StatUpMove + def initialize(battle, move) + super + @statUp = [:SPECIAL_ATTACK, 3] + end +end + +#=============================================================================== +# Reduces the user's HP by half of max, and sets its Attack to maximum. +# (Belly Drum) +#=============================================================================== +class PokeBattle_Move_03A < PokeBattle_Move + def pbMoveFailed?(user, targets) + hpLoss = [user.totalhp / 2, 1].max + if user.hp <= hpLoss + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if !user.pbCanRaiseStatStage?(:ATTACK, user, self, true) + return false + end + + def pbEffectGeneral(user) + hpLoss = [user.totalhp / 2, 1].max + user.pbReduceHP(hpLoss, false) + if user.hasActiveAbility?(:CONTRARY) + user.stages[:ATTACK] = -6 + @battle.pbCommonAnimation("StatDown", user) + @battle.pbDisplay(_INTL("{1} cut its own HP and minimized its Attack!", user.pbThis)) + else + user.stages[:ATTACK] = 6 + @battle.pbCommonAnimation("StatUp", user) + @battle.pbDisplay(_INTL("{1} cut its own HP and maximized its Attack!", user.pbThis)) + end + user.pbItemHPHealCheck + end +end + +#=============================================================================== +# Decreases the user's Attack and Defense by 1 stage each. (Superpower) +#=============================================================================== +class PokeBattle_Move_03B < PokeBattle_StatDownMove + def initialize(battle, move) + super + @statDown = [:ATTACK, 1, :DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the user's Defense and Special Defense by 1 stage each. +# (Close Combat, Dragon Ascent) +#=============================================================================== +class PokeBattle_Move_03C < PokeBattle_StatDownMove + def initialize(battle, move) + super + @statDown = [:DEFENSE, 1, :SPECIAL_DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the user's Defense, Special Defense and Speed by 1 stage each. +# (V-create) +#=============================================================================== +class PokeBattle_Move_03D < PokeBattle_StatDownMove + def initialize(battle, move) + super + @statDown = [:SPEED, 1, :DEFENSE, 1, :SPECIAL_DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the user's Speed by 1 stage. (Hammer Arm, Ice Hammer) +#=============================================================================== +class PokeBattle_Move_03E < PokeBattle_StatDownMove + def initialize(battle, move) + super + @statDown = [:SPEED, 1] + end +end + +#=============================================================================== +# Decreases the user's Special Attack by 2 stages. +#=============================================================================== +class PokeBattle_Move_03F < PokeBattle_StatDownMove + def initialize(battle, move) + super + @statDown = [:SPECIAL_ATTACK, 2] + end +end + +#=============================================================================== +# Increases the target's Special Attack by 1 stage. Confuses the target. (Flatter) +#=============================================================================== +class PokeBattle_Move_040 < PokeBattle_Move + def pbMoveFailed?(user, targets) + failed = true + targets.each do |b| + next if !b.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) && + !b.pbCanConfuse?(user, false, self) + failed = false + break + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + if target.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) + target.pbRaiseStatStage(:SPECIAL_ATTACK, 1, user) + end + target.pbConfuse if target.pbCanConfuse?(user, false, self) + end +end + +#=============================================================================== +# Increases the target's Attack by 2 stages. Confuses the target. (Swagger) +#=============================================================================== +class PokeBattle_Move_041 < PokeBattle_Move + def pbMoveFailed?(user, targets) + failed = true + targets.each do |b| + next if !b.pbCanRaiseStatStage?(:ATTACK, user, self) && + !b.pbCanConfuse?(user, false, self) + failed = false + break + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + if target.pbCanRaiseStatStage?(:ATTACK, user, self) + target.pbRaiseStatStage(:ATTACK, 2, user) + end + target.pbConfuse if target.pbCanConfuse?(user, false, self) + end +end + +#=============================================================================== +# Decreases the target's Attack by 1 stage. +#=============================================================================== +class PokeBattle_Move_042 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:ATTACK, 1] + end +end + +#=============================================================================== +# Decreases the target's Defense by 1 stage. +#=============================================================================== +class PokeBattle_Move_043 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the target's Speed by 1 stage. +#=============================================================================== +class PokeBattle_Move_044 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:SPEED, 1] + end + + def pbBaseDamage(baseDmg, user, target) + if @id == :BULLDOZE && @battle.field.terrain == :Grassy + baseDmg = (baseDmg / 2.0).round + end + return baseDmg + end +end + +#=============================================================================== +# Decreases the target's Special Attack by 1 stage. +#=============================================================================== +class PokeBattle_Move_045 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:SPECIAL_ATTACK, 1] + end +end + +#=============================================================================== +# Decreases the target's Special Defense by 1 stage. +#=============================================================================== +class PokeBattle_Move_046 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:SPECIAL_DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the target's accuracy by 1 stage. +#=============================================================================== +class PokeBattle_Move_047 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:ACCURACY, 1] + end +end + +#=============================================================================== +# Decreases the target's evasion by 1 stage OR 2 stages. (Sweet Scent) +#=============================================================================== +class PokeBattle_Move_048 < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:EVASION, (Settings::MECHANICS_GENERATION >= 6) ? 2 : 1] + end +end + +#=============================================================================== +# Decreases the target's evasion by 1 stage. Ends all barriers and entry +# hazards for the target's side OR on both sides. (Defog) +#=============================================================================== +class PokeBattle_Move_049 < PokeBattle_TargetStatDownMove + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @statDown = [:EVASION, 1] + end + + def pbFailsAgainstTarget?(user, target) + targetSide = target.pbOwnSide + targetOpposingSide = target.pbOpposingSide + return false if targetSide.effects[PBEffects::AuroraVeil] > 0 || + targetSide.effects[PBEffects::LightScreen] > 0 || + targetSide.effects[PBEffects::Reflect] > 0 || + targetSide.effects[PBEffects::Mist] > 0 || + targetSide.effects[PBEffects::Safeguard] > 0 + return false if targetSide.effects[PBEffects::StealthRock] || + targetSide.effects[PBEffects::Spikes] > 0 || + targetSide.effects[PBEffects::ToxicSpikes] > 0 || + targetSide.effects[PBEffects::StickyWeb] + return false if Settings::MECHANICS_GENERATION >= 6 && + (targetOpposingSide.effects[PBEffects::StealthRock] || + targetOpposingSide.effects[PBEffects::Spikes] > 0 || + targetOpposingSide.effects[PBEffects::ToxicSpikes] > 0 || + targetOpposingSide.effects[PBEffects::StickyWeb]) + return false if Settings::MECHANICS_GENERATION >= 8 && @battle.field.terrain != :None + return super + end + + def pbEffectAgainstTarget(user, target) + if target.pbCanLowerStatStage?(@statDown[0], user, self) + target.pbLowerStatStage(@statDown[0], @statDown[1], user) + end + if target.pbOwnSide.effects[PBEffects::AuroraVeil] > 0 + target.pbOwnSide.effects[PBEffects::AuroraVeil] = 0 + @battle.pbDisplay(_INTL("{1}'s Aurora Veil wore off!", target.pbTeam)) + end + if target.pbOwnSide.effects[PBEffects::LightScreen] > 0 + target.pbOwnSide.effects[PBEffects::LightScreen] = 0 + @battle.pbDisplay(_INTL("{1}'s Light Screen wore off!", target.pbTeam)) + end + if target.pbOwnSide.effects[PBEffects::Reflect] > 0 + target.pbOwnSide.effects[PBEffects::Reflect] = 0 + @battle.pbDisplay(_INTL("{1}'s Reflect wore off!", target.pbTeam)) + end + if target.pbOwnSide.effects[PBEffects::Mist] > 0 + target.pbOwnSide.effects[PBEffects::Mist] = 0 + @battle.pbDisplay(_INTL("{1}'s Mist faded!", target.pbTeam)) + end + if target.pbOwnSide.effects[PBEffects::Safeguard] > 0 + target.pbOwnSide.effects[PBEffects::Safeguard] = 0 + @battle.pbDisplay(_INTL("{1} is no longer protected by Safeguard!!", target.pbTeam)) + end + if target.pbOwnSide.effects[PBEffects::StealthRock] || + (Settings::MECHANICS_GENERATION >= 6 && + target.pbOpposingSide.effects[PBEffects::StealthRock]) + target.pbOwnSide.effects[PBEffects::StealthRock] = false + target.pbOpposingSide.effects[PBEffects::StealthRock] = false if Settings::MECHANICS_GENERATION >= 6 + @battle.pbDisplay(_INTL("{1} blew away stealth rocks!", user.pbThis)) + end + if target.pbOwnSide.effects[PBEffects::Spikes] > 0 || + (Settings::MECHANICS_GENERATION >= 6 && + target.pbOpposingSide.effects[PBEffects::Spikes] > 0) + target.pbOwnSide.effects[PBEffects::Spikes] = 0 + target.pbOpposingSide.effects[PBEffects::Spikes] = 0 if Settings::MECHANICS_GENERATION >= 6 + @battle.pbDisplay(_INTL("{1} blew away spikes!", user.pbThis)) + end + if target.pbOwnSide.effects[PBEffects::ToxicSpikes] > 0 || + (Settings::MECHANICS_GENERATION >= 6 && + target.pbOpposingSide.effects[PBEffects::ToxicSpikes] > 0) + target.pbOwnSide.effects[PBEffects::ToxicSpikes] = 0 + target.pbOpposingSide.effects[PBEffects::ToxicSpikes] = 0 if Settings::MECHANICS_GENERATION >= 6 + @battle.pbDisplay(_INTL("{1} blew away poison spikes!", user.pbThis)) + end + if target.pbOwnSide.effects[PBEffects::StickyWeb] || + (Settings::MECHANICS_GENERATION >= 6 && + target.pbOpposingSide.effects[PBEffects::StickyWeb]) + target.pbOwnSide.effects[PBEffects::StickyWeb] = false + target.pbOpposingSide.effects[PBEffects::StickyWeb] = false if Settings::MECHANICS_GENERATION >= 6 + @battle.pbDisplay(_INTL("{1} blew away sticky webs!", user.pbThis)) + end + if Settings::MECHANICS_GENERATION >= 8 && @battle.field.terrain != :None + case @battle.field.terrain + when :Electric + @battle.pbDisplay(_INTL("The electricity disappeared from the battlefield.")) + when :Grassy + @battle.pbDisplay(_INTL("The grass disappeared from the battlefield.")) + when :Misty + @battle.pbDisplay(_INTL("The mist disappeared from the battlefield.")) + when :Psychic + @battle.pbDisplay(_INTL("The weirdness disappeared from the battlefield.")) + end + @battle.field.terrain = :None + end + end +end + +#=============================================================================== +# Decreases the target's Attack and Defense by 1 stage each. (Tickle) +#=============================================================================== +class PokeBattle_Move_04A < PokeBattle_TargetMultiStatDownMove + def initialize(battle, move) + super + @statDown = [:ATTACK, 1, :DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the target's Attack by 2 stages. (Charm, Feather Dance) +#=============================================================================== +class PokeBattle_Move_04B < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:ATTACK, 2] + end +end + +#=============================================================================== +# Decreases the target's Defense by 2 stages. (Screech) +#=============================================================================== +class PokeBattle_Move_04C < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:DEFENSE, 2] + end +end + +#=============================================================================== +# Decreases the target's Speed by 2 stages. (Cotton Spore, Scary Face, String Shot) +#=============================================================================== +class PokeBattle_Move_04D < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + inc = 2 + inc = 1 if @id == :STRINGSHOT && Settings::MECHANICS_GENERATION <= 5 + @statDown = [:SPEED, inc] + end +end + +#=============================================================================== +# Decreases the target's Special Attack by 2 stages. Only works on the opposite +# gender. (Captivate) +#=============================================================================== +class PokeBattle_Move_04E < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:SPECIAL_ATTACK, 2] + end + + def pbFailsAgainstTarget?(user, target) + return true if super + return false if damagingMove? + if user.gender == 2 || target.gender == 2 || user.gender == target.gender + @battle.pbDisplay(_INTL("{1} is unaffected!", target.pbThis)) + return true + end + if target.hasActiveAbility?(:OBLIVIOUS) && !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} is unaffected!", target.pbThis)) + else + @battle.pbDisplay(_INTL("{1}'s {2} prevents romance!", target.pbThis, target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return true + end + return false + end + + def pbAdditionalEffect(user, target) + return if user.gender == 2 || target.gender == 2 || user.gender == target.gender + return if target.hasActiveAbility?(:OBLIVIOUS) && !@battle.moldBreaker + super + end +end + +#=============================================================================== +# Decreases the target's Special Defense by 2 stages. +#=============================================================================== +class PokeBattle_Move_04F < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:SPECIAL_DEFENSE, 2] + end +end + +#=============================================================================== +# Resets all target's stat stages to 0. (Clear Smog) +#=============================================================================== +class PokeBattle_Move_050 < PokeBattle_Move + def pbEffectAgainstTarget(user, target) + if target.damageState.calcDamage > 0 && !target.damageState.substitute && + target.hasAlteredStatStages? + target.pbResetStatStages + @battle.pbDisplay(_INTL("{1}'s stat changes were removed!", target.pbThis)) + end + end +end + +#=============================================================================== +# Resets all stat stages for all battlers to 0. (Haze) +#=============================================================================== +class PokeBattle_Move_051 < PokeBattle_Move + def pbMoveFailed?(user, targets) + failed = true + @battle.eachBattler do |b| + failed = false if b.hasAlteredStatStages? + break if !failed + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.eachBattler { |b| b.pbResetStatStages } + @battle.pbDisplay(_INTL("All stat changes were eliminated!")) + end +end + +#=============================================================================== +# User and target swap their Attack and Special Attack stat stages. (Power Swap) +#=============================================================================== +class PokeBattle_Move_052 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + [:ATTACK, :SPECIAL_ATTACK].each do |s| + user.stages[s], target.stages[s] = target.stages[s], user.stages[s] + end + @battle.pbDisplay(_INTL("{1} switched all changes to its Attack and Sp. Atk with the target!", user.pbThis)) + end +end + +#=============================================================================== +# User and target swap their Defense and Special Defense stat stages. (Guard Swap) +#=============================================================================== +class PokeBattle_Move_053 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + [:DEFENSE, :SPECIAL_DEFENSE].each do |s| + user.stages[s], target.stages[s] = target.stages[s], user.stages[s] + end + @battle.pbDisplay(_INTL("{1} switched all changes to its Defense and Sp. Def with the target!", user.pbThis)) + end +end + +#=============================================================================== +# User and target swap all their stat stages. (Heart Swap) +#=============================================================================== +class PokeBattle_Move_054 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + GameData::Stat.each_battle do |s| + user.stages[s.id], target.stages[s.id] = target.stages[s.id], user.stages[s.id] + end + @battle.pbDisplay(_INTL("{1} switched stat changes with the target!", user.pbThis)) + end +end + +#=============================================================================== +# User copies the target's stat stages. (Psych Up) +#=============================================================================== +class PokeBattle_Move_055 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + GameData::Stat.each_battle { |s| user.stages[s.id] = target.stages[s.id] } + if Settings::NEW_CRITICAL_HIT_RATE_MECHANICS + user.effects[PBEffects::FocusEnergy] = target.effects[PBEffects::FocusEnergy] + user.effects[PBEffects::LaserFocus] = target.effects[PBEffects::LaserFocus] + end + @battle.pbDisplay(_INTL("{1} copied {2}'s stat changes!", user.pbThis, target.pbThis(true))) + end +end + +#=============================================================================== +# For 5 rounds, user's and ally's stat stages cannot be lowered by foes. (Mist) +#=============================================================================== +class PokeBattle_Move_056 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOwnSide.effects[PBEffects::Mist] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::Mist] = 5 + @battle.pbDisplay(_INTL("{1} became shrouded in mist!", user.pbTeam)) + end +end + +#=============================================================================== +# Swaps the user's Attack and Defense stats. (Power Trick) +#=============================================================================== +class PokeBattle_Move_057 < PokeBattle_Move + def pbEffectGeneral(user) + user.attack, user.defense = user.defense, user.attack + user.effects[PBEffects::PowerTrick] = !user.effects[PBEffects::PowerTrick] + @battle.pbDisplay(_INTL("{1} switched its Attack and Defense!", user.pbThis)) + end +end + +#=============================================================================== +# Averages the user's and target's Attack. +# Averages the user's and target's Special Attack. (Power Split) +#=============================================================================== +class PokeBattle_Move_058 < PokeBattle_Move + def pbEffectAgainstTarget(user, target) + newatk = ((user.attack + target.attack) / 2).floor + newspatk = ((user.spatk + target.spatk) / 2).floor + user.attack = target.attack = newatk + user.spatk = target.spatk = newspatk + @battle.pbDisplay(_INTL("{1} shared its power with the target!", user.pbThis)) + end +end + +#=============================================================================== +# Averages the user's and target's Defense. +# Averages the user's and target's Special Defense. (Guard Split) +#=============================================================================== +class PokeBattle_Move_059 < PokeBattle_Move + def pbEffectAgainstTarget(user, target) + newdef = ((user.defense + target.defense) / 2).floor + newspdef = ((user.spdef + target.spdef) / 2).floor + user.defense = target.defense = newdef + user.spdef = target.spdef = newspdef + @battle.pbDisplay(_INTL("{1} shared its guard with the target!", user.pbThis)) + end +end + +#=============================================================================== +# Averages the user's and target's current HP. (Pain Split) +#=============================================================================== +class PokeBattle_Move_05A < PokeBattle_Move + def pbEffectAgainstTarget(user, target) + newHP = (user.hp + target.hp) / 2 + if user.hp > newHP; + user.pbReduceHP(user.hp - newHP, false, false) + elsif user.hp < newHP; + user.pbRecoverHP(newHP - user.hp, false) + end + if target.hp > newHP; + target.pbReduceHP(target.hp - newHP, false, false) + elsif target.hp < newHP; + target.pbRecoverHP(newHP - target.hp, false) + end + @battle.pbDisplay(_INTL("The battlers shared their pain!")) + user.pbItemHPHealCheck + target.pbItemHPHealCheck + end +end + +#=============================================================================== +# For 4 rounds, doubles the Speed of all battlers on the user's side. (Tailwind) +#=============================================================================== +class PokeBattle_Move_05B < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOwnSide.effects[PBEffects::Tailwind] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::Tailwind] = 4 + @battle.pbDisplay(_INTL("The Tailwind blew from behind {1}!", user.pbTeam(true))) + end +end + +#=============================================================================== +# This move turns into the last move used by the target, until user switches +# out. (Mimic) +#=============================================================================== +class PokeBattle_Move_05C < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @moveBlacklist = [ + "014", # Chatter + "0B6", # Metronome + # Struggle + "002", # Struggle + # Moves that affect the moveset + "05C", # Mimic + "05D", # Sketch + "069" # Transform + ] + end + + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Transform] || !user.pbHasMove?(@id) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + lastMoveData = GameData::Move.try_get(target.lastRegularMoveUsed) + if !lastMoveData || + user.pbHasMove?(target.lastRegularMoveUsed) || + @moveBlacklist.include?(lastMoveData.function_code) || + lastMoveData.type == :SHADOW + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + user.eachMoveWithIndex do |m, i| + next if m.id != @id + newMove = Pokemon::Move.new(target.lastRegularMoveUsed) + user.moves[i] = PokeBattle_Move.from_pokemon_move(@battle, newMove) + @battle.pbDisplay(_INTL("{1} learned {2}!", user.pbThis, newMove.name)) + user.pbCheckFormOnMovesetChange + break + end + end +end + +#=============================================================================== +# This move permanently turns into the last move used by the target. (Sketch) +#=============================================================================== +class PokeBattle_Move_05D < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @moveBlacklist = [ + "014", # Chatter + "05D", # Sketch (this move) + # Struggle + "002" # Struggle + ] + end + + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Transform] || !user.pbHasMove?(@id) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + lastMoveData = GameData::Move.try_get(target.lastRegularMoveUsed) + if !lastMoveData || + user.pbHasMove?(target.lastRegularMoveUsed) || + @moveBlacklist.include?(lastMoveData.function_code) || + lastMoveData.type == :SHADOW + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + user.eachMoveWithIndex do |m, i| + next if m.id != @id + newMove = Pokemon::Move.new(target.lastRegularMoveUsed) + user.pokemon.moves[i] = newMove + user.moves[i] = PokeBattle_Move.from_pokemon_move(@battle, newMove) + @battle.pbDisplay(_INTL("{1} learned {2}!", user.pbThis, newMove.name)) + user.pbCheckFormOnMovesetChange + break + end + end +end + +#=============================================================================== +# Changes user's type to that of a random user's move, except a type the user +# already has (even partially), OR changes to the user's first move's type. +# (Conversion) +#=============================================================================== +class PokeBattle_Move_05E < PokeBattle_Move + def pbMoveFailed?(user, targets) + if !user.canChangeType? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + userTypes = user.pbTypes(true) + @newTypes = [] + user.eachMoveWithIndex do |m, i| + break if Settings::MECHANICS_GENERATION >= 6 && i > 0 + next if GameData::Type.get(m.type).pseudo_type + next if userTypes.include?(m.type) + @newTypes.push(m.type) if !@newTypes.include?(m.type) + end + if @newTypes.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + newType = @newTypes[@battle.pbRandom(@newTypes.length)] + user.pbChangeTypes(newType) + typeName = GameData::Type.get(newType).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", user.pbThis, typeName)) + end +end + +#=============================================================================== +# Changes user's type to a random one that resists/is immune to the last move +# used by the target. (Conversion 2) +#=============================================================================== +class PokeBattle_Move_05F < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + if !user.canChangeType? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if !target.lastMoveUsed || !target.lastMoveUsedType || + GameData::Type.get(target.lastMoveUsedType).pseudo_type + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + @newTypes = [] + GameData::Type.each do |t| + next if t.pseudo_type || user.pbHasType?(t.id) || + !Effectiveness.resistant_type?(target.lastMoveUsedType, t.id) + @newTypes.push(t.id) + end + if @newTypes.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + newType = @newTypes[@battle.pbRandom(@newTypes.length)] + user.pbChangeTypes(newType) + typeName = GameData::Type.get(newType).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", user.pbThis, typeName)) + end +end + +#=============================================================================== +# Changes user's type depending on the environment. (Camouflage) +#=============================================================================== +class PokeBattle_Move_060 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if !user.canChangeType? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + @newType = :NORMAL + checkedTerrain = false + case @battle.field.terrain + when :Electric + if GameData::Type.exists?(:ELECTRIC) + @newType = :ELECTRIC + checkedTerrain = true + end + when :Grassy + if GameData::Type.exists?(:GRASS) + @newType = :GRASS + checkedTerrain = true + end + when :Misty + if GameData::Type.exists?(:FAIRY) + @newType = :FAIRY + checkedTerrain = true + end + when :Psychic + if GameData::Type.exists?(:PSYCHIC) + @newType = :PSYCHIC + checkedTerrain = true + end + end + if !checkedTerrain + case @battle.environment + when :Grass, :TallGrass, :Grass_alt1,:Grass_alt2,:Grass_alt3, + @newType = :GRASS + when :MovingWater, :StillWater, :Puddle, :Underwater + @newType = :WATER + when :Cave + @newType = :ROCK + when :Rock, :Sand + @newType = :GROUND + when :Forest, :ForestGrass + @newType = :BUG + when :Snow, :Ice + @newType = :ICE + when :Volcano + @newType = :FIRE + when :Graveyard + @newType = :GHOST + when :Sky + @newType = :FLYING + when :Space + @newType = :DRAGON + when :UltraSpace + @newType = :PSYCHIC + end + end + @newType = :NORMAL if !GameData::Type.exists?(@newType) + if !GameData::Type.exists?(@newType) || !user.pbHasOtherType?(@newType) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbChangeTypes(@newType) + typeName = GameData::Type.get(@newType).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", user.pbThis, typeName)) + end +end + +#=============================================================================== +# Target becomes Water type. (Soak) +#=============================================================================== +class PokeBattle_Move_061 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if !target.canChangeType? || !GameData::Type.exists?(:WATER) || + !target.pbHasOtherType?(:WATER) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.pbChangeTypes(:WATER) + typeName = GameData::Type.get(:WATER).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", target.pbThis, typeName)) + end +end + +#=============================================================================== +# User copes target's types. (Reflect Type) +#=============================================================================== +class PokeBattle_Move_062 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + if !user.canChangeType? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + newTypes = target.pbTypes(true) + if newTypes.length == 0 # Target has no type to copy + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if user.pbTypes == target.pbTypes && + user.effects[PBEffects::Type3] == target.effects[PBEffects::Type3] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + user.pbChangeTypes(target) + @battle.pbDisplay(_INTL("{1}'s type changed to match {2}'s!", + user.pbThis, target.pbThis(true))) + end +end + +#=============================================================================== +# Target's ability becomes Simple. (Simple Beam) +#=============================================================================== +class PokeBattle_Move_063 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if !GameData::Ability.exists?(:SIMPLE) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if target.unstoppableAbility? || [:TRUANT, :SIMPLE].include?(target.ability) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + @battle.pbShowAbilitySplash(target, true, false) + oldAbil = target.ability + target.ability = :SIMPLE + @battle.pbReplaceAbilitySplash(target) + @battle.pbDisplay(_INTL("{1} acquired {2}!", target.pbThis, target.abilityName)) + @battle.pbHideAbilitySplash(target) + target.pbOnAbilityChanged(oldAbil) + end +end + +#=============================================================================== +# Target's ability becomes Insomnia. (Worry Seed) +#=============================================================================== +class PokeBattle_Move_064 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if !GameData::Ability.exists?(:INSOMNIA) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if target.unstoppableAbility? || [:TRUANT, :INSOMNIA].include?(target.ability_id) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + @battle.pbShowAbilitySplash(target, true, false) + oldAbil = target.ability + target.ability = :INSOMNIA + @battle.pbReplaceAbilitySplash(target) + @battle.pbDisplay(_INTL("{1} acquired {2}!", target.pbThis, target.abilityName)) + @battle.pbHideAbilitySplash(target) + target.pbOnAbilityChanged(oldAbil) + end +end + +#=============================================================================== +# User copies target's ability. (Role Play) +#=============================================================================== +class PokeBattle_Move_065 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + if user.unstoppableAbility? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if !target.ability || user.ability == target.ability + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.ungainableAbility? || + [:POWEROFALCHEMY, :RECEIVER, :TRACE, :WONDERGUARD].include?(target.ability_id) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + @battle.pbShowAbilitySplash(user, true, false) + oldAbil = user.ability + user.ability = target.ability + @battle.pbReplaceAbilitySplash(user) + @battle.pbDisplay(_INTL("{1} copied {2}'s {3}!", + user.pbThis, target.pbThis(true), target.abilityName)) + @battle.pbHideAbilitySplash(user) + user.pbOnAbilityChanged(oldAbil) + user.pbEffectsOnSwitchIn + end +end + +#=============================================================================== +# Target copies user's ability. (Entrainment) +#=============================================================================== +class PokeBattle_Move_066 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if !user.ability + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if user.ungainableAbility? || + [:POWEROFALCHEMY, :RECEIVER, :TRACE].include?(user.ability_id) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if target.unstoppableAbility? || target.ability == :TRUANT + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + @battle.pbShowAbilitySplash(target, true, false) + oldAbil = target.ability + target.ability = user.ability + @battle.pbReplaceAbilitySplash(target) + @battle.pbDisplay(_INTL("{1} acquired {2}!", target.pbThis, target.abilityName)) + @battle.pbHideAbilitySplash(target) + target.pbOnAbilityChanged(oldAbil) + target.pbEffectsOnSwitchIn + end +end + +#=============================================================================== +# User and target swap abilities. (Skill Swap) +#=============================================================================== +class PokeBattle_Move_067 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + if !user.ability + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if user.unstoppableAbility? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if user.ungainableAbility? || user.ability == :WONDERGUARD + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if !target.ability || + (user.ability == target.ability && Settings::MECHANICS_GENERATION <= 5) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.unstoppableAbility? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.ungainableAbility? || target.ability == :WONDERGUARD + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + if user.opposes?(target) + @battle.pbShowAbilitySplash(user, false, false) + @battle.pbShowAbilitySplash(target, true, false) + end + oldUserAbil = user.ability + oldTargetAbil = target.ability + user.ability = oldTargetAbil + target.ability = oldUserAbil + if user.opposes?(target) + @battle.pbReplaceAbilitySplash(user) + @battle.pbReplaceAbilitySplash(target) + end + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} swapped Abilities with its target!", user.pbThis)) + else + @battle.pbDisplay(_INTL("{1} swapped its {2} Ability with its target's {3} Ability!", + user.pbThis, target.abilityName, user.abilityName)) + end + if user.opposes?(target) + @battle.pbHideAbilitySplash(user) + @battle.pbHideAbilitySplash(target) + end + user.pbOnAbilityChanged(oldUserAbil) + target.pbOnAbilityChanged(oldTargetAbil) + user.pbEffectsOnSwitchIn + target.pbEffectsOnSwitchIn + end +end + +#=============================================================================== +# Target's ability is negated. (Gastro Acid) +#=============================================================================== +class PokeBattle_Move_068 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if target.unstoppableAbility? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::GastroAcid] = true + target.effects[PBEffects::Truant] = false + @battle.pbDisplay(_INTL("{1}'s Ability was suppressed!", target.pbThis)) + target.pbOnAbilityChanged(target.ability) + end +end + +#=============================================================================== +# User transforms into the target. (Transform) +#=============================================================================== +class PokeBattle_Move_069 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Transform] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if target.effects[PBEffects::Transform] || + target.effects[PBEffects::Illusion] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + user.pbTransform(target) + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + super + @battle.scene.pbChangePokemon(user, targets[0].pokemon) + end +end + +#=============================================================================== +# Inflicts a fixed 20HP damage. (Sonic Boom) +#=============================================================================== +class PokeBattle_Move_06A < PokeBattle_FixedDamageMove + def pbFixedDamage(user, target) + return 20 + end +end + +#=============================================================================== +# Inflicts a fixed 40HP damage. (Dragon Rage) +#=============================================================================== +class PokeBattle_Move_06B < PokeBattle_FixedDamageMove + def pbFixedDamage(user, target) + return 40 + end +end + +#=============================================================================== +# Halves the target's current HP. (Nature's Madness, Super Fang) +#=============================================================================== +class PokeBattle_Move_06C < PokeBattle_FixedDamageMove + def pbFixedDamage(user, target) + return (target.hp / 2.0).round + end +end + +#=============================================================================== +# Inflicts damage equal to the user's level. (Night Shade, Seismic Toss) +#=============================================================================== +class PokeBattle_Move_06D < PokeBattle_FixedDamageMove + def pbFixedDamage(user, target) + return user.level + end +end + +#=============================================================================== +# Inflicts damage to bring the target's HP down to equal the user's HP. (Endeavor) +#=============================================================================== +class PokeBattle_Move_06E < PokeBattle_FixedDamageMove + def pbFailsAgainstTarget?(user, target) + if user.hp >= target.hp + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbNumHits(user, targets) + ; return 1; + end + + def pbFixedDamage(user, target) + return target.hp - user.hp + end +end + +#=============================================================================== +# Inflicts damage between 0.5 and 1.5 times the user's level. (Psywave) +#=============================================================================== +class PokeBattle_Move_06F < PokeBattle_FixedDamageMove + def pbFixedDamage(user, target) + min = (user.level / 2).floor + max = (user.level * 3 / 2).floor + return min + @battle.pbRandom(max - min + 1) + end +end + +#=============================================================================== +# OHKO. Accuracy increases by difference between levels of user and target. +#=============================================================================== +class PokeBattle_Move_070 < PokeBattle_FixedDamageMove + def hitsDiggingTargets? + return @id == :FISSURE; + end + + def pbFailsAgainstTarget?(user, target) + if target.level > user.level + @battle.pbDisplay(_INTL("{1} is unaffected!", target.pbThis)) + return true + end + if target.hasActiveAbility?(:STURDY) && !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("But it failed to affect {1}!", target.pbThis(true))) + else + @battle.pbDisplay(_INTL("But it failed to affect {1} because of its {2}!", + target.pbThis(true), target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return true + end + if Settings::MECHANICS_GENERATION >= 7 && @id == :SHEERCOLD && target.pbHasType?(:ICE) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbAccuracyCheck(user, target) + acc = @accuracy + user.level - target.level + acc -= 10 if Settings::MECHANICS_GENERATION >= 7 && @id == :SHEERCOLD && !user.pbHasType?(:ICE) + return @battle.pbRandom(100) < acc + end + + def pbFixedDamage(user, target) + return target.totalhp + end + + def pbHitEffectivenessMessages(user, target, numTargets = 1) + super + if target.fainted? + @battle.pbDisplay(_INTL("It's a one-hit KO!")) + end + end +end + +#=============================================================================== +# Counters a physical move used against the user this round, with 2x the power. +# (Counter) +#=============================================================================== +class PokeBattle_Move_071 < PokeBattle_FixedDamageMove + def pbAddTarget(targets, user) + t = user.effects[PBEffects::CounterTarget] + return if t < 0 || !user.opposes?(t) + user.pbAddTarget(targets, user, @battle.battlers[t], self, false) + end + + def pbMoveFailed?(user, targets) + if targets.length == 0 + @battle.pbDisplay(_INTL("But there was no target...")) + return true + end + return false + end + + def pbFixedDamage(user, target) + dmg = user.effects[PBEffects::Counter] * 2 + dmg = 1 if dmg == 0 + return dmg + end +end + +#=============================================================================== +# Counters a specical move used against the user this round, with 2x the power. +# (Mirror Coat) +#=============================================================================== +class PokeBattle_Move_072 < PokeBattle_FixedDamageMove + def pbAddTarget(targets, user) + t = user.effects[PBEffects::MirrorCoatTarget] + return if t < 0 || !user.opposes?(t) + user.pbAddTarget(targets, user, @battle.battlers[t], self, false) + end + + def pbMoveFailed?(user, targets) + if targets.length == 0 + @battle.pbDisplay(_INTL("But there was no target...")) + return true + end + return false + end + + def pbFixedDamage(user, target) + dmg = user.effects[PBEffects::MirrorCoat] * 2 + dmg = 1 if dmg == 0 + return dmg + end +end + +#=============================================================================== +# Counters the last damaging move used against the user this round, with 1.5x +# the power. (Metal Burst) +#=============================================================================== +class PokeBattle_Move_073 < PokeBattle_FixedDamageMove + def pbAddTarget(targets, user) + return if user.lastFoeAttacker.length == 0 + lastAttacker = user.lastFoeAttacker.last + return if lastAttacker < 0 || !user.opposes?(lastAttacker) + user.pbAddTarget(targets, user, @battle.battlers[lastAttacker], self, false) + end + + def pbMoveFailed?(user, targets) + if targets.length == 0 + @battle.pbDisplay(_INTL("But there was no target...")) + return true + end + return false + end + + def pbFixedDamage(user, target) + dmg = (user.lastHPLostFromFoe * 1.5).floor + dmg = 1 if dmg == 0 + return dmg + end +end + +#=============================================================================== +# The target's ally loses 1/16 of its max HP. (Flame Burst) +#=============================================================================== +class PokeBattle_Move_074 < PokeBattle_Move + def pbEffectWhenDealingDamage(user, target) + hitAlly = [] + target.eachAlly do |b| + next if !b.near?(target.index) + next if !b.takesIndirectDamage? + hitAlly.push([b.index, b.hp]) + b.pbReduceHP(b.totalhp / 16, false) + end + if hitAlly.length == 2 + @battle.pbDisplay(_INTL("The bursting flame hit {1} and {2}!", + @battle.battlers[hitAlly[0][0]].pbThis(true), + @battle.battlers[hitAlly[1][0]].pbThis(true))) + elsif hitAlly.length > 0 + hitAlly.each do |b| + @battle.pbDisplay(_INTL("The bursting flame hit {1}!", + @battle.battlers[b[0]].pbThis(true))) + end + end + switchedAlly = [] + hitAlly.each do |b| + @battle.battlers[b[0]].pbItemHPHealCheck + if @battle.battlers[b[0]].pbAbilitiesOnDamageTaken(b[1]) + switchedAlly.push(@battle.battlers[b[0]]) + end + end + switchedAlly.each { |b| b.pbEffectsOnSwitchIn(true) } + end +end + +#=============================================================================== +# Power is doubled if the target is using Dive. Hits some semi-invulnerable +# targets. (Surf) +#=============================================================================== +class PokeBattle_Move_075 < PokeBattle_Move + def hitsDivingTargets? + return true; + end + + def pbModifyDamage(damageMult, user, target) + damageMult *= 2 if target.inTwoTurnAttack?("0CB") # Dive + return damageMult + end +end + +#=============================================================================== +# Power is doubled if the target is using Dig. Power is halved if Grassy Terrain +# is in effect. Hits some semi-invulnerable targets. (Earthquake) +#=============================================================================== +class PokeBattle_Move_076 < PokeBattle_Move + def hitsDiggingTargets? + return true; + end + + def pbModifyDamage(damageMult, user, target) + damageMult *= 2 if target.inTwoTurnAttack?("0CA") # Dig + damageMult /= 2 if @battle.field.terrain == :Grassy + return damageMult + end +end + +#=============================================================================== +# Power is doubled if the target is using Bounce, Fly or Sky Drop. Hits some +# semi-invulnerable targets. (Gust) +#=============================================================================== +class PokeBattle_Move_077 < PokeBattle_Move + def hitsFlyingTargets? + return true; + end + + def pbBaseDamage(baseDmg, user, target) + baseDmg *= 2 if target.inTwoTurnAttack?("0C9", "0CC", "0CE") || # Fly/Bounce/Sky Drop + target.effects[PBEffects::SkyDrop] >= 0 + return baseDmg + end +end + +#=============================================================================== +# Power is doubled if the target is using Bounce, Fly or Sky Drop. Hits some +# semi-invulnerable targets. May make the target flinch. (Twister) +#=============================================================================== +class PokeBattle_Move_078 < PokeBattle_FlinchMove + def hitsFlyingTargets? + return true; + end + + def pbBaseDamage(baseDmg, user, target) + baseDmg *= 2 if target.inTwoTurnAttack?("0C9", "0CC", "0CE") || # Fly/Bounce/Sky Drop + target.effects[PBEffects::SkyDrop] >= 0 + return baseDmg + end +end + +#=============================================================================== +# Power is doubled if Fusion Flare has already been used this round. (Fusion Bolt) +#=============================================================================== +class PokeBattle_Move_079 < PokeBattle_Move + def pbChangeUsageCounters(user, specialUsage) + @doublePower = @battle.field.effects[PBEffects::FusionFlare] + super + end + + def pbBaseDamageMultiplier(damageMult, user, target) + damageMult *= 2 if @doublePower + return damageMult + end + + def pbEffectGeneral(user) + @battle.field.effects[PBEffects::FusionBolt] = true + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + hitNum = 1 if (targets.length > 0 && targets[0].damageState.critical) || + @doublePower # Charged anim + super + end +end + +#=============================================================================== +# Power is doubled if Fusion Bolt has already been used this round. (Fusion Flare) +#=============================================================================== +class PokeBattle_Move_07A < PokeBattle_Move + def pbChangeUsageCounters(user, specialUsage) + @doublePower = @battle.field.effects[PBEffects::FusionBolt] + super + end + + def pbBaseDamageMultiplier(damageMult, user, target) + damageMult *= 2 if @doublePower + return damageMult + end + + def pbEffectGeneral(user) + @battle.field.effects[PBEffects::FusionFlare] = true + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + hitNum = 1 if (targets.length > 0 && targets[0].damageState.critical) || + @doublePower # Charged anim + super + end +end + +#=============================================================================== +# Power is doubled if the target is poisoned. (Venoshock) +#=============================================================================== +class PokeBattle_Move_07B < PokeBattle_Move + def pbBaseDamage(baseDmg, user, target) + if target.poisoned? && + (target.effects[PBEffects::Substitute] == 0 || ignoresSubstitute?(user)) + baseDmg *= 2 + end + return baseDmg + end +end + +#=============================================================================== +# Power is doubled if the target is paralyzed. Cures the target of paralysis. +# (Smelling Salts) +#=============================================================================== +class PokeBattle_Move_07C < PokeBattle_Move + def pbBaseDamage(baseDmg, user, target) + if target.paralyzed? && + (target.effects[PBEffects::Substitute] == 0 || ignoresSubstitute?(user)) + baseDmg *= 2 + end + return baseDmg + end + + def pbEffectAfterAllHits(user, target) + return if target.fainted? + return if target.damageState.unaffected || target.damageState.substitute + return if target.status != :PARALYSIS + target.pbCureStatus + end +end + +#=============================================================================== +# Power is doubled if the target is asleep. Wakes the target up. (Wake-Up Slap) +#=============================================================================== +class PokeBattle_Move_07D < PokeBattle_Move + def pbBaseDamage(baseDmg, user, target) + if target.asleep? && + (target.effects[PBEffects::Substitute] == 0 || ignoresSubstitute?(user)) + baseDmg *= 2 + end + return baseDmg + end + + def pbEffectAfterAllHits(user, target) + return if target.fainted? + return if target.damageState.unaffected || target.damageState.substitute + return if target.status != :SLEEP + target.pbCureStatus + end +end + +#=============================================================================== +# Power is doubled if the user is burned, poisoned or paralyzed. (Facade) +# Burn's halving of Attack is negated (new mechanics). +#=============================================================================== +class PokeBattle_Move_07E < PokeBattle_Move + def damageReducedByBurn? + return Settings::MECHANICS_GENERATION <= 5; + end + + def pbBaseDamage(baseDmg, user, target) + baseDmg *= 2 if user.poisoned? || user.burned? || user.paralyzed? + return baseDmg + end +end + +#=============================================================================== +# Power is doubled if the target has a status problem. (Hex) +#=============================================================================== +class PokeBattle_Move_07F < PokeBattle_Move + def pbBaseDamage(baseDmg, user, target) + if target.pbHasAnyStatus? && + (target.effects[PBEffects::Substitute] == 0 || ignoresSubstitute?(user)) + baseDmg *= 2 + end + return baseDmg + end +end + +################# +# Fusion Swap +# ############### +# class PokeBattle_Move_XXX < PokeBattle_Move +# def pbMoveFailed?(user,targets) +# if targets[0].effects[PBEffects::Transform] +# @battle.pbDisplay(_INTL("But it failed!")) +# return true +# end +# return false +# end +# +# def pbFailsAgainstTarget?(user,target) +# if target.effects[PBEffects::Transform] || +# target.effects[PBEffects::Illusion] || +# !target.pokemon.isFusion? +# @battle.pbDisplay(_INTL("But it failed!")) +# return true +# end +# return false +# end +# +# def pbEffectAgainstTarget(user,target) +# body = getBasePokemonID(target.pokemon.species, true) +# head = getBasePokemonID(target.pokemon.species, false) +# newspecies = (head) * Settings::NB_POKEMON + body +# target.pbTransform(newspecies) +# end +# +# def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) +# super +# @battle.scene.pbChangePokemon(user,targets[0].pokemon) +# end +# end diff --git a/Data/Scripts/011_Battle/002_Move/006_Move_Effects_080-0FF.rb b/Data/Scripts/011_Battle/002_Move/006_Move_Effects_080-0FF.rb new file mode 100644 index 000000000..1dca76cc8 --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/006_Move_Effects_080-0FF.rb @@ -0,0 +1,3723 @@ +#=============================================================================== +# Power is doubled if the target's HP is down to 1/2 or less. (Brine) +#=============================================================================== +class PokeBattle_Move_080 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if target.hp<=target.totalhp/2 + return baseDmg + end +end + + + +#=============================================================================== +# Power is doubled if the user has lost HP due to the target's move this round. +# (Avalanche, Revenge) +#=============================================================================== +class PokeBattle_Move_081 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if user.lastAttacker.include?(target.index) + return baseDmg + end +end + + + +#=============================================================================== +# Power is doubled if the target has already lost HP this round. (Assurance) +#=============================================================================== +class PokeBattle_Move_082 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if target.tookDamage + return baseDmg + end +end + + + +#=============================================================================== +# Power is doubled if a user's ally has already used this move this round. (Round) +# If an ally is about to use the same move, make it go next, ignoring priority. +#=============================================================================== +class PokeBattle_Move_083 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if user.pbOwnSide.effects[PBEffects::Round] + return baseDmg + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::Round] = true + user.eachAlly do |b| + next if @battle.choices[b.index][0]!=:UseMove || b.movedThisRound? + next if @battle.choices[b.index][2].function!=@function + b.effects[PBEffects::MoveNext] = true + b.effects[PBEffects::Quash] = 0 + break + end + end +end + + + +#=============================================================================== +# Power is doubled if the target has already moved this round. (Payback) +#=============================================================================== +class PokeBattle_Move_084 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + if @battle.choices[target.index][0]!=:None && + ((@battle.choices[target.index][0]!=:UseMove && + @battle.choices[target.index][0]!=:Shift) || target.movedThisRound?) + baseDmg *= 2 + end + return baseDmg + end +end + + + +#=============================================================================== +# Power is doubled if a user's teammate fainted last round. (Retaliate) +#=============================================================================== +class PokeBattle_Move_085 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + lrf = user.pbOwnSide.effects[PBEffects::LastRoundFainted] + baseDmg *= 2 if lrf>=0 && lrf==@battle.turnCount-1 + return baseDmg + end +end + + + +#=============================================================================== +# Power is doubled if the user has no held item. (Acrobatics) +#=============================================================================== +class PokeBattle_Move_086 < PokeBattle_Move + def pbBaseDamageMultiplier(damageMult,user,target) + damageMult *= 2 if !user.item + return damageMult + end +end + + + +#=============================================================================== +# Power is doubled in weather. Type changes depending on the weather. (Weather Ball) +#=============================================================================== +class PokeBattle_Move_087 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if @battle.pbWeather != :None + return baseDmg + end + + def pbBaseType(user) + ret = :NORMAL + case @battle.pbWeather + when :Sun, :HarshSun + ret = :FIRE if GameData::Type.exists?(:FIRE) + when :Rain, :HeavyRain + ret = :WATER if GameData::Type.exists?(:WATER) + when :Sandstorm + ret = :ROCK if GameData::Type.exists?(:ROCK) + when :Hail + ret = :ICE if GameData::Type.exists?(:ICE) + end + return ret + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + t = pbBaseType(user) + hitNum = 1 if t == :FIRE # Type-specific anims + hitNum = 2 if t == :WATER + hitNum = 3 if t == :ROCK + hitNum = 4 if t == :ICE + super + end +end + + + +#=============================================================================== +# Interrupts a foe switching out or using U-turn/Volt Switch/Parting Shot. Power +# is doubled in that case. (Pursuit) +# (Handled in Battle's pbAttackPhase): Makes this attack happen before switching. +#=============================================================================== +class PokeBattle_Move_088 < PokeBattle_Move + def pbAccuracyCheck(user,target) + return true if @battle.switching + return super + end + + def pbBaseDamage(baseDmg,user,target) + baseDmg *= 2 if @battle.switching + return baseDmg + end +end + + + +#=============================================================================== +# Power increases with the user's happiness. (Return) +#=============================================================================== +class PokeBattle_Move_089 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + return [(user.happiness*2/5).floor,1].max + end +end + + + +#=============================================================================== +# Power decreases with the user's happiness. (Frustration) +#=============================================================================== +class PokeBattle_Move_08A < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + return [((255-user.happiness)*2/5).floor,1].max + end +end + + + +#=============================================================================== +# Power increases with the user's HP. (Eruption, Water Spout) +#=============================================================================== +class PokeBattle_Move_08B < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + return [150*user.hp/user.totalhp,1].max + end +end + + + +#=============================================================================== +# Power increases with the target's HP. (Crush Grip, Wring Out) +#=============================================================================== +class PokeBattle_Move_08C < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + return [120*target.hp/target.totalhp,1].max + end +end + + + +#=============================================================================== +# Power increases the quicker the target is than the user. (Gyro Ball) +#=============================================================================== +class PokeBattle_Move_08D < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + return [[(25*target.pbSpeed/user.pbSpeed).floor,150].min,1].max + end +end + + + +#=============================================================================== +# Power increases with the user's positive stat changes (ignores negative ones). +# (Power Trip, Stored Power) +#=============================================================================== +class PokeBattle_Move_08E < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + mult = 1 + GameData::Stat.each_battle { |s| mult += user.stages[s.id] if user.stages[s.id] > 0 } + return 20 * mult + end +end + + + +#=============================================================================== +# Power increases with the target's positive stat changes (ignores negative ones). +# (Punishment) +#=============================================================================== +class PokeBattle_Move_08F < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + mult = 3 + GameData::Stat.each_battle { |s| mult += target.stages[s.id] if target.stages[s.id] > 0 } + return [20 * mult, 200].min + end +end + + + +#=============================================================================== +# Power and type depends on the user's IVs. (Hidden Power) +#=============================================================================== +class PokeBattle_Move_090 < PokeBattle_Move + def pbBaseType(user) + hp = pbHiddenPower(user,user.pokemon.hiddenPowerType) + return hp[0] + end + + def pbBaseDamage(baseDmg,user,target) + return super if Settings::MECHANICS_GENERATION >= 6 + hp = pbHiddenPower(user,user.pokemon.hiddenPowerType) + return hp[1] + end +end + + + +def pbHiddenPower(pkmn,forcedType=nil) + # NOTE: This allows Hidden Power to be Fairy-type (if you have that type in + # your game). I don't care that the official games don't work like that. + iv = pkmn.iv + idxType = 0; power = 60 + types = [] + GameData::Type.each { |t| types.push(t.id) if !t.pseudo_type && ![:NORMAL, :SHADOW].include?(t.id)} + types.sort! { |a, b| GameData::Type.get(a).id_number <=> GameData::Type.get(b).id_number } + idxType |= (iv[:HP]&1) + idxType |= (iv[:ATTACK]&1)<<1 + idxType |= (iv[:DEFENSE]&1)<<2 + idxType |= (iv[:SPEED]&1)<<3 + idxType |= (iv[:SPECIAL_ATTACK]&1)<<4 + idxType |= (iv[:SPECIAL_DEFENSE]&1)<<5 + idxType = (types.length-1)*idxType/63 + type = types[idxType] + if Settings::MECHANICS_GENERATION <= 5 + powerMin = 30 + powerMax = 70 + power |= (iv[:HP]&2)>>1 + power |= (iv[:ATTACK]&2) + power |= (iv[:DEFENSE]&2)<<1 + power |= (iv[:SPEED]&2)<<2 + power |= (iv[:SPECIAL_ATTACK]&2)<<3 + power |= (iv[:SPECIAL_DEFENSE]&2)<<4 + power = powerMin+(powerMax-powerMin)*power/63 + end + if forcedType != nil + return [forcedType,power] + end + return [type,power] +end + + + +#=============================================================================== +# Power doubles for each consecutive use. (Fury Cutter) +#=============================================================================== +class PokeBattle_Move_091 < PokeBattle_Move + def pbChangeUsageCounters(user,specialUsage) + oldVal = user.effects[PBEffects::FuryCutter] + super + maxMult = 1 + while (@baseDamage<<(maxMult-1))<160 + maxMult += 1 # 1-4 for base damage of 20, 1-3 for base damage of 40 + end + user.effects[PBEffects::FuryCutter] = (oldVal>=maxMult) ? maxMult : oldVal+1 + end + + def pbBaseDamage(baseDmg,user,target) + return baseDmg<<(user.effects[PBEffects::FuryCutter]-1) + end +end + + + +#=============================================================================== +# Power is multiplied by the number of consecutive rounds in which this move was +# used by any Pokémon on the user's side. (Echoed Voice) +#=============================================================================== +class PokeBattle_Move_092 < PokeBattle_Move + def pbChangeUsageCounters(user,specialUsage) + oldVal = user.pbOwnSide.effects[PBEffects::EchoedVoiceCounter] + super + if !user.pbOwnSide.effects[PBEffects::EchoedVoiceUsed] + user.pbOwnSide.effects[PBEffects::EchoedVoiceCounter] = (oldVal>=5) ? 5 : oldVal+1 + end + user.pbOwnSide.effects[PBEffects::EchoedVoiceUsed] = true + end + + def pbBaseDamage(baseDmg,user,target) + return baseDmg*user.pbOwnSide.effects[PBEffects::EchoedVoiceCounter] # 1-5 + end +end + + + +#=============================================================================== +# User rages until the start of a round in which they don't use this move. (Rage) +# (Handled in Battler's pbProcessMoveAgainstTarget): Ups rager's Attack by 1 +# stage each time it loses HP due to a move. +#=============================================================================== +class PokeBattle_Move_093 < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::Rage] = true + end +end + + + +#=============================================================================== +# Randomly damages or heals the target. (Present) +# NOTE: Apparently a Normal Gem should be consumed even if this move will heal, +# but I think that's silly so I've omitted that effect. +#=============================================================================== +class PokeBattle_Move_094 < PokeBattle_Move + def pbOnStartUse(user,targets) + @presentDmg = 0 # 0 = heal, >0 = damage + r = @battle.pbRandom(100) + if r<40; @presentDmg = 40 + elsif r<70; @presentDmg = 80 + elsif r<80; @presentDmg = 120 + end + end + + def pbFailsAgainstTarget?(user,target) + return false if @presentDmg>0 + if !target.canHeal? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbDamagingMove? + return false if @presentDmg==0 + return super + end + + def pbBaseDamage(baseDmg,user,target) + return @presentDmg + end + + def pbEffectAgainstTarget(user,target) + return if @presentDmg>0 + target.pbRecoverHP(target.totalhp/4) + @battle.pbDisplay(_INTL("{1}'s HP was restored.",target.pbThis)) + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + hitNum = 1 if @presentDmg==0 # Healing anim + super + end +end + + + +#=============================================================================== +# Power is chosen at random. Power is doubled if the target is using Dig. Hits +# some semi-invulnerable targets. (Magnitude) +#=============================================================================== +class PokeBattle_Move_095 < PokeBattle_Move + def hitsDiggingTargets?; return true; end + + def pbOnStartUse(user,targets) + baseDmg = [10,30,50,70,90,110,150] + magnitudes = [ + 4, + 5,5, + 6,6,6,6, + 7,7,7,7,7,7, + 8,8,8,8, + 9,9, + 10 + ] + magni = magnitudes[@battle.pbRandom(magnitudes.length)] + @magnitudeDmg = baseDmg[magni-4] + @battle.pbDisplay(_INTL("Magnitude {1}!",magni)) + end + + def pbBaseDamage(baseDmg,user,target) + return @magnitudeDmg + end + + def pbModifyDamage(damageMult,user,target) + damageMult *= 2 if target.inTwoTurnAttack?("0CA") # Dig + damageMult /= 2 if @battle.field.terrain == :Grassy + return damageMult + end +end + + + +#=============================================================================== +# Power and type depend on the user's held berry. Destroys the berry. +# (Natural Gift) +#=============================================================================== +class PokeBattle_Move_096 < PokeBattle_Move + def initialize(battle,move) + super + @typeArray = { + :NORMAL => [:CHILANBERRY], + :FIRE => [:CHERIBERRY, :BLUKBERRY, :WATMELBERRY, :OCCABERRY], + :WATER => [:CHESTOBERRY, :NANABBERRY, :DURINBERRY, :PASSHOBERRY], + :ELECTRIC => [:PECHABERRY, :WEPEARBERRY, :BELUEBERRY, :WACANBERRY], + :GRASS => [:RAWSTBERRY, :PINAPBERRY, :RINDOBERRY, :LIECHIBERRY], + :ICE => [:ASPEARBERRY, :POMEGBERRY, :YACHEBERRY, :GANLONBERRY], + :FIGHTING => [:LEPPABERRY, :KELPSYBERRY, :CHOPLEBERRY, :SALACBERRY], + :POISON => [:ORANBERRY, :QUALOTBERRY, :KEBIABERRY, :PETAYABERRY], + :GROUND => [:PERSIMBERRY, :HONDEWBERRY, :SHUCABERRY, :APICOTBERRY], + :FLYING => [:LUMBERRY, :GREPABERRY, :COBABERRY, :LANSATBERRY], + :PSYCHIC => [:SITRUSBERRY, :TAMATOBERRY, :PAYAPABERRY, :STARFBERRY], + :BUG => [:FIGYBERRY, :CORNNBERRY, :TANGABERRY, :ENIGMABERRY], + :ROCK => [:WIKIBERRY, :MAGOSTBERRY, :CHARTIBERRY, :MICLEBERRY], + :GHOST => [:MAGOBERRY, :RABUTABERRY, :KASIBBERRY, :CUSTAPBERRY], + :DRAGON => [:AGUAVBERRY, :NOMELBERRY, :HABANBERRY, :JABOCABERRY], + :DARK => [:IAPAPABERRY, :SPELONBERRY, :COLBURBERRY, :ROWAPBERRY, :MARANGABERRY], + :STEEL => [:RAZZBERRY, :PAMTREBERRY, :BABIRIBERRY], + :FAIRY => [:ROSELIBERRY, :KEEBERRY] + } + @damageArray = { + 60 => [:CHERIBERRY, :CHESTOBERRY, :PECHABERRY, :RAWSTBERRY, :ASPEARBERRY, + :LEPPABERRY, :ORANBERRY, :PERSIMBERRY, :LUMBERRY, :SITRUSBERRY, + :FIGYBERRY, :WIKIBERRY, :MAGOBERRY, :AGUAVBERRY, :IAPAPABERRY, + :RAZZBERRY, :OCCABERRY, :PASSHOBERRY, :WACANBERRY, :RINDOBERRY, + :YACHEBERRY, :CHOPLEBERRY, :KEBIABERRY, :SHUCABERRY, :COBABERRY, + :PAYAPABERRY, :TANGABERRY, :CHARTIBERRY, :KASIBBERRY, :HABANBERRY, + :COLBURBERRY, :BABIRIBERRY, :CHILANBERRY, :ROSELIBERRY], + 70 => [:BLUKBERRY, :NANABBERRY, :WEPEARBERRY, :PINAPBERRY, :POMEGBERRY, + :KELPSYBERRY, :QUALOTBERRY, :HONDEWBERRY, :GREPABERRY, :TAMATOBERRY, + :CORNNBERRY, :MAGOSTBERRY, :RABUTABERRY, :NOMELBERRY, :SPELONBERRY, + :PAMTREBERRY], + 80 => [:WATMELBERRY, :DURINBERRY, :BELUEBERRY, :LIECHIBERRY, :GANLONBERRY, + :SALACBERRY, :PETAYABERRY, :APICOTBERRY, :LANSATBERRY, :STARFBERRY, + :ENIGMABERRY, :MICLEBERRY, :CUSTAPBERRY, :JABOCABERRY, :ROWAPBERRY, + :KEEBERRY, :MARANGABERRY] + } + end + + def pbMoveFailed?(user,targets) + # NOTE: Unnerve does not stop a Pokémon using this move. + item = user.item + if !item || !item.is_berry? || !user.itemActive? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + # NOTE: The AI calls this method via pbCalcType, but it involves user.item + # which here is assumed to be not nil (because item.id is called). Since + # the AI won't want to use it if the user has no item anyway, perhaps + # this is good enough. + def pbBaseType(user) + item = user.item + ret = :NORMAL + if item + @typeArray.each do |type, items| + next if !items.include?(item.id) + ret = type if GameData::Type.exists?(type) + break + end + end + return ret + end + + # This is a separate method so that the AI can use it as well + def pbNaturalGiftBaseDamage(heldItem) + ret = 1 + @damageArray.each do |dmg, items| + next if !items.include?(heldItem) + ret = dmg + ret += 20 if Settings::MECHANICS_GENERATION >= 6 + break + end + return ret + end + + def pbBaseDamage(baseDmg,user,target) + return pbNaturalGiftBaseDamage(user.item.id) + end + + def pbEndOfMoveUsageEffect(user,targets,numHits,switchedBattlers) + # NOTE: The item is consumed even if this move was Protected against or it + # missed. The item is not consumed if the target was switched out by + # an effect like a target's Red Card. + # NOTE: There is no item consumption animation. + user.pbConsumeItem(true,true,false) if user.item + end +end + + + +#=============================================================================== +# Power increases the less PP this move has. (Trump Card) +#=============================================================================== +class PokeBattle_Move_097 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + dmgs = [200,80,60,50,40] + ppLeft = [@pp,dmgs.length-1].min # PP is reduced before the move is used + return dmgs[ppLeft] + end +end + + + +#=============================================================================== +# Power increases the less HP the user has. (Flail, Reversal) +#=============================================================================== +class PokeBattle_Move_098 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + ret = 20 + n = 48*user.hp/user.totalhp + if n<2; ret = 200 + elsif n<5; ret = 150 + elsif n<10; ret = 100 + elsif n<17; ret = 80 + elsif n<33; ret = 40 + end + return ret + end +end + + + +#=============================================================================== +# Power increases the quicker the user is than the target. (Electro Ball) +#=============================================================================== +class PokeBattle_Move_099 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + ret = 40 + n = user.pbSpeed/target.pbSpeed + if n>=4; ret = 150 + elsif n>=3; ret = 120 + elsif n>=2; ret = 80 + elsif n>=1; ret = 60 + end + return ret + end +end + + + +#=============================================================================== +# Power increases the heavier the target is. (Grass Knot, Low Kick) +#=============================================================================== +class PokeBattle_Move_09A < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + ret = 20 + weight = target.pbWeight + if weight>=2000; ret = 120 + elsif weight>=1000; ret = 100 + elsif weight>=500; ret = 80 + elsif weight>=250; ret = 60 + elsif weight>=100; ret = 40 + end + return ret + end +end + + + +#=============================================================================== +# Power increases the heavier the user is than the target. (Heat Crash, Heavy Slam) +# Does double damage and has perfect accuracy if the target is Minimized. +#=============================================================================== +class PokeBattle_Move_09B < PokeBattle_Move + def tramplesMinimize?(param=1) + return true if Settings::MECHANICS_GENERATION >= 7 # Perfect accuracy and double damage + return super + end + + def pbBaseDamage(baseDmg,user,target) + ret = 40 + n = (user.pbWeight/target.pbWeight).floor + if n>=5; ret = 120 + elsif n>=4; ret = 100 + elsif n>=3; ret = 80 + elsif n>=2; ret = 60 + end + return ret + end +end + + + +#=============================================================================== +# Powers up the ally's attack this round by 1.5. (Helping Hand) +#=============================================================================== +class PokeBattle_Move_09C < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbFailsAgainstTarget?(user,target) + if target.fainted? || target.effects[PBEffects::HelpingHand] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedTargetAlreadyMoved?(target) + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::HelpingHand] = true + @battle.pbDisplay(_INTL("{1} is ready to help {2}!",user.pbThis,target.pbThis(true))) + end +end + + + +#=============================================================================== +# Weakens Electric attacks. (Mud Sport) +#=============================================================================== +class PokeBattle_Move_09D < PokeBattle_Move + def pbMoveFailed?(user,targets) + if Settings::MECHANICS_GENERATION >= 6 + if @battle.field.effects[PBEffects::MudSportField]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + else + @battle.eachBattler do |b| + next if !b.effects[PBEffects::MudSport] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + end + return false + end + + def pbEffectGeneral(user) + if Settings::MECHANICS_GENERATION >= 6 + @battle.field.effects[PBEffects::MudSportField] = 5 + else + user.effects[PBEffects::MudSport] = true + end + @battle.pbDisplay(_INTL("Electricity's power was weakened!")) + end +end + + + +#=============================================================================== +# Weakens Fire attacks. (Water Sport) +#=============================================================================== +class PokeBattle_Move_09E < PokeBattle_Move + def pbMoveFailed?(user,targets) + if Settings::MECHANICS_GENERATION >= 6 + if @battle.field.effects[PBEffects::WaterSportField]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + else + @battle.eachBattler do |b| + next if !b.effects[PBEffects::WaterSport] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + end + return false + end + + def pbEffectGeneral(user) + if Settings::MECHANICS_GENERATION >= 6 + @battle.field.effects[PBEffects::WaterSportField] = 5 + else + user.effects[PBEffects::WaterSport] = true + end + @battle.pbDisplay(_INTL("Fire's power was weakened!")) + end +end + + + +#=============================================================================== +# Type depends on the user's held item. (Judgment, Multi-Attack, Techno Blast) +#=============================================================================== +class PokeBattle_Move_09F < PokeBattle_Move + def initialize(battle,move) + super + if @id == :JUDGMENT + @itemTypes = { + :FISTPLATE => :FIGHTING, + :SKYPLATE => :FLYING, + :TOXICPLATE => :POISON, + :EARTHPLATE => :GROUND, + :STONEPLATE => :ROCK, + :INSECTPLATE => :BUG, + :SPOOKYPLATE => :GHOST, + :IRONPLATE => :STEEL, + :FLAMEPLATE => :FIRE, + :SPLASHPLATE => :WATER, + :MEADOWPLATE => :GRASS, + :ZAPPLATE => :ELECTRIC, + :MINDPLATE => :PSYCHIC, + :ICICLEPLATE => :ICE, + :DRACOPLATE => :DRAGON, + :DREADPLATE => :DARK, + :PIXIEPLATE => :FAIRY + } + elsif @id == :TECHNOBLAST + @itemTypes = { + :SHOCKDRIVE => :ELECTRIC, + :BURNDRIVE => :FIRE, + :CHILLDRIVE => :ICE, + :DOUSEDRIVE => :WATER + } + elsif @id == :MULTIATTACK + @itemTypes = { + :FIGHTINGMEMORY => :FIGHTING, + :FLYINGMEMORY => :FLYING, + :POISONMEMORY => :POISON, + :GROUNDMEMORY => :GROUND, + :ROCKMEMORY => :ROCK, + :BUGMEMORY => :BUG, + :GHOSTMEMORY => :GHOST, + :STEELMEMORY => :STEEL, + :FIREMEMORY => :FIRE, + :WATERMEMORY => :WATER, + :GRASSMEMORY => :GRASS, + :ELECTRICMEMORY => :ELECTRIC, + :PSYCHICMEMORY => :PSYCHIC, + :ICEMEMORY => :ICE, + :DRAGONMEMORY => :DRAGON, + :DARKMEMORY => :DARK, + :FAIRYMEMORY => :FAIRY + } + end + end + + def pbBaseType(user) + ret = :NORMAL + if user.itemActive? + @itemTypes.each do |item, itemType| + next if user.item != item + ret = itemType if GameData::Type.exists?(itemType) + break + end + end + return ret + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + if @id == :TECHNOBLAST # Type-specific anim + t = pbBaseType(user) + hitNum = 0 + hitNum = 1 if t == :ELECTRIC + hitNum = 2 if t == :FIRE + hitNum = 3 if t == :ICE + hitNum = 4 if t == :WATER + end + super + end +end + + + +#=============================================================================== +# This attack is always a critical hit. (Frost Breath, Storm Throw) +#=============================================================================== +class PokeBattle_Move_0A0 < PokeBattle_Move + def pbCritialOverride(user,target); return 1; end +end + + + +#=============================================================================== +# For 5 rounds, foes' attacks cannot become critical hits. (Lucky Chant) +#=============================================================================== +class PokeBattle_Move_0A1 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if user.pbOwnSide.effects[PBEffects::LuckyChant]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::LuckyChant] = 5 + @battle.pbDisplay(_INTL("The Lucky Chant shielded {1} from critical hits!",user.pbTeam(true))) + end +end + + + +#=============================================================================== +# For 5 rounds, lowers power of physical attacks against the user's side. +# (Reflect) +#=============================================================================== +class PokeBattle_Move_0A2 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if user.pbOwnSide.effects[PBEffects::Reflect]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::Reflect] = 5 + user.pbOwnSide.effects[PBEffects::Reflect] = 8 if user.hasActiveItem?(:LIGHTCLAY) + @battle.pbDisplay(_INTL("{1} raised {2}'s Defense!",@name,user.pbTeam(true))) + end +end + + + +#=============================================================================== +# For 5 rounds, lowers power of special attacks against the user's side. (Light Screen) +#=============================================================================== +class PokeBattle_Move_0A3 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if user.pbOwnSide.effects[PBEffects::LightScreen]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::LightScreen] = 5 + user.pbOwnSide.effects[PBEffects::LightScreen] = 8 if user.hasActiveItem?(:LIGHTCLAY) + @battle.pbDisplay(_INTL("{1} raised {2}'s Special Defense!",@name,user.pbTeam(true))) + end +end + + + +#=============================================================================== +# Effect depends on the environment. (Secret Power) +#=============================================================================== +class PokeBattle_Move_0A4 < PokeBattle_Move + def flinchingMove?; return [6,10,12].include?(@secretPower); end + + def pbOnStartUse(user,targets) + # NOTE: This is Gen 7's list plus some of Gen 6 plus a bit of my own. + @secretPower = 0 # Body Slam, paralysis + case @battle.field.terrain + when :Electric + @secretPower = 1 # Thunder Shock, paralysis + when :Grassy + @secretPower = 2 # Vine Whip, sleep + when :Misty + @secretPower = 3 # Fairy Wind, lower Sp. Atk by 1 + when :Psychic + @secretPower = 4 # Confusion, lower Speed by 1 + else + case @battle.environment + when :Grass, :TallGrass, :Forest, :ForestGrass + @secretPower = 2 # (Same as Grassy Terrain) + when :MovingWater, :StillWater, :Underwater + @secretPower = 5 # Water Pulse, lower Attack by 1 + when :Puddle + @secretPower = 6 # Mud Shot, lower Speed by 1 + when :Cave + @secretPower = 7 # Rock Throw, flinch + when :Rock, :Sand + @secretPower = 8 # Mud-Slap, lower Acc by 1 + when :Snow, :Ice + @secretPower = 9 # Ice Shard, freeze + when :Volcano + @secretPower = 10 # Incinerate, burn + when :Graveyard + @secretPower = 11 # Shadow Sneak, flinch + when :Sky + @secretPower = 12 # Gust, lower Speed by 1 + when :Space + @secretPower = 13 # Swift, flinch + when :UltraSpace + @secretPower = 14 # Psywave, lower Defense by 1 + end + end + end + + # NOTE: This intentionally doesn't use def pbAdditionalEffect, because that + # method is called per hit and this move's additional effect only occurs + # once per use, after all the hits have happened (two hits are possible + # via Parental Bond). + def pbEffectAfterAllHits(user,target) + return if target.fainted? + return if target.damageState.unaffected || target.damageState.substitute + chance = pbAdditionalEffectChance(user,target) + return if @battle.pbRandom(100)>=chance + case @secretPower + when 2 + target.pbSleep if target.pbCanSleep?(user,false,self) + when 10 + target.pbBurn(user) if target.pbCanBurn?(user,false,self) + when 0, 1 + target.pbParalyze(user) if target.pbCanParalyze?(user,false,self) + when 9 + target.pbFreeze if target.pbCanFreeze?(user,false,self) + when 5 + if target.pbCanLowerStatStage?(:ATTACK,user,self) + target.pbLowerStatStage(:ATTACK,1,user) + end + when 14 + if target.pbCanLowerStatStage?(:DEFENSE,user,self) + target.pbLowerStatStage(:DEFENSE,1,user) + end + when 3 + if target.pbCanLowerStatStage?(:SPECIAL_ATTACK,user,self) + target.pbLowerStatStage(:SPECIAL_ATTACK,1,user) + end + when 4, 6, 12 + if target.pbCanLowerStatStage?(:SPEED,user,self) + target.pbLowerStatStage(:SPEED,1,user) + end + when 8 + if target.pbCanLowerStatStage?(:ACCURACY,user,self) + target.pbLowerStatStage(:ACCURACY,1,user) + end + when 7, 11, 13 + target.pbFlinch(user) + end + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + id = :BODYSLAM # Environment-specific anim + case @secretPower + when 1 then id = :THUNDERSHOCK if GameData::Move.exists?(:THUNDERSHOCK) + when 2 then id = :VINEWHIP if GameData::Move.exists?(:VINEWHIP) + when 3 then id = :FAIRYWIND if GameData::Move.exists?(:FAIRYWIND) + when 4 then id = :CONFUSIO if GameData::Move.exists?(:CONFUSION) + when 5 then id = :WATERPULSE if GameData::Move.exists?(:WATERPULSE) + when 6 then id = :MUDSHOT if GameData::Move.exists?(:MUDSHOT) + when 7 then id = :ROCKTHROW if GameData::Move.exists?(:ROCKTHROW) + when 8 then id = :MUDSLAP if GameData::Move.exists?(:MUDSLAP) + when 9 then id = :ICESHARD if GameData::Move.exists?(:ICESHARD) + when 10 then id = :INCINERATE if GameData::Move.exists?(:INCINERATE) + when 11 then id = :SHADOWSNEAK if GameData::Move.exists?(:SHADOWSNEAK) + when 12 then id = :GUST if GameData::Move.exists?(:GUST) + when 13 then id = :SWIFT if GameData::Move.exists?(:SWIFT) + when 14 then id = :PSYWAVE if GameData::Move.exists?(:PSYWAVE) + end + super + end +end + + + +#=============================================================================== +# Always hits. +#=============================================================================== +class PokeBattle_Move_0A5 < PokeBattle_Move + def pbAccuracyCheck(user,target); return true; end +end + + + +#=============================================================================== +# User's attack next round against the target will definitely hit. +# (Lock-On, Mind Reader) +#=============================================================================== +class PokeBattle_Move_0A6 < PokeBattle_Move + def pbEffectAgainstTarget(user,target) + user.effects[PBEffects::LockOn] = 2 + user.effects[PBEffects::LockOnPos] = target.index + @battle.pbDisplay(_INTL("{1} took aim at {2}!",user.pbThis,target.pbThis(true))) + end +end + + + +#=============================================================================== +# Target's evasion stat changes are ignored from now on. (Foresight, Odor Sleuth) +# Normal and Fighting moves have normal effectiveness against the Ghost-type target. +#=============================================================================== +class PokeBattle_Move_0A7 < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::Foresight] = true + @battle.pbDisplay(_INTL("{1} was identified!",target.pbThis)) + end +end + + + +#=============================================================================== +# Target's evasion stat changes are ignored from now on. (Miracle Eye) +# Psychic moves have normal effectiveness against the Dark-type target. +#=============================================================================== +class PokeBattle_Move_0A8 < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::MiracleEye] = true + @battle.pbDisplay(_INTL("{1} was identified!",target.pbThis)) + end +end + + + +#=============================================================================== +# This move ignores target's Defense, Special Defense and evasion stat changes. +# (Chip Away, Darkest Lariat, Sacred Sword) +#=============================================================================== +class PokeBattle_Move_0A9 < PokeBattle_Move + def pbCalcAccuracyMultipliers(user,target,multipliers) + super + modifiers[:evasion_stage] = 0 + end + + def pbGetDefenseStats(user,target) + ret1, _ret2 = super + return ret1, 6 # Def/SpDef stat stage + end +end + + + +#=============================================================================== +# User is protected against moves with the "B" flag this round. (Detect, Protect) +#=============================================================================== +class PokeBattle_Move_0AA < PokeBattle_ProtectMove + def initialize(battle,move) + super + @effect = PBEffects::Protect + end +end + + + +#=============================================================================== +# User's side is protected against moves with priority greater than 0 this round. +# (Quick Guard) +#=============================================================================== +class PokeBattle_Move_0AB < PokeBattle_ProtectMove + def initialize(battle,move) + super + @effect = PBEffects::QuickGuard + @sidedEffect = true + end +end + + + +#=============================================================================== +# User's side is protected against moves that target multiple battlers this round. +# (Wide Guard) +#=============================================================================== +class PokeBattle_Move_0AC < PokeBattle_ProtectMove + def initialize(battle,move) + super + @effect = PBEffects::WideGuard + @sidedEffect = true + end +end + + + +#=============================================================================== +# Ends target's protections immediately. (Feint) +#=============================================================================== +class PokeBattle_Move_0AD < PokeBattle_Move + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::BanefulBunker] = false + target.effects[PBEffects::KingsShield] = false + target.effects[PBEffects::Protect] = false + target.effects[PBEffects::SpikyShield] = false + target.pbOwnSide.effects[PBEffects::CraftyShield] = false + target.pbOwnSide.effects[PBEffects::MatBlock] = false + target.pbOwnSide.effects[PBEffects::QuickGuard] = false + target.pbOwnSide.effects[PBEffects::WideGuard] = false + end +end + + + +#=============================================================================== +# Uses the last move that the target used. (Mirror Move) +#=============================================================================== +class PokeBattle_Move_0AE < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + def callsAnotherMove?; return true; end + + def pbFailsAgainstTarget?(user,target) + if !target.lastRegularMoveUsed || + !GameData::Move.get(target.lastRegularMoveUsed).flags[/e/] # Not copyable by Mirror Move + @battle.pbDisplay(_INTL("The mirror move failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + user.pbUseMoveSimple(target.lastRegularMoveUsed,target.index) + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + # No animation + end +end + + + +#=============================================================================== +# Uses the last move that was used. (Copycat) +#=============================================================================== +class PokeBattle_Move_0AF < PokeBattle_Move + def callsAnotherMove?; return true; end + + def initialize(battle,move) + super + @moveBlacklist = [ + # Struggle, Chatter, Belch + "002", # Struggle + "014", # Chatter + "158", # Belch # Not listed on Bulbapedia + # Moves that affect the moveset + "05C", # Mimic + "05D", # Sketch + "069", # Transform + # Counter moves + "071", # Counter + "072", # Mirror Coat + "073", # Metal Burst # Not listed on Bulbapedia + # Helping Hand, Feint (always blacklisted together, don't know why) + "09C", # Helping Hand + "0AD", # Feint + # Protection moves + "0AA", # Detect, Protect + "0AB", # Quick Guard # Not listed on Bulbapedia + "0AC", # Wide Guard # Not listed on Bulbapedia + "0E8", # Endure + "149", # Mat Block + "14A", # Crafty Shield # Not listed on Bulbapedia + "14B", # King's Shield + "14C", # Spiky Shield + "168", # Baneful Bunker + # Moves that call other moves + "0AE", # Mirror Move + "0AF", # Copycat (this move) + "0B0", # Me First + "0B3", # Nature Power # Not listed on Bulbapedia + "0B4", # Sleep Talk + "0B5", # Assist + "0B6", # Metronome + # Move-redirecting and stealing moves + "0B1", # Magic Coat # Not listed on Bulbapedia + "0B2", # Snatch + "117", # Follow Me, Rage Powder + "16A", # Spotlight + # Set up effects that trigger upon KO + "0E6", # Grudge # Not listed on Bulbapedia + "0E7", # Destiny Bond + # Held item-moving moves + "0F1", # Covet, Thief + "0F2", # Switcheroo, Trick + "0F3", # Bestow + # Moves that start focussing at the start of the round + "115", # Focus Punch + "171", # Shell Trap + "172", # Beak Blast + # Event moves that do nothing + "133", # Hold Hands + "134" # Celebrate + ] + if Settings::MECHANICS_GENERATION >= 6 + @moveBlacklist += [ + # Target-switching moves + "0EB", # Roar, Whirlwind + "0EC" # Circle Throw, Dragon Tail + ] + end + end + + def pbChangeUsageCounters(user,specialUsage) + super + @copied_move = @battle.lastMoveUsed + end + + def pbMoveFailed?(user,targets) + if !@copied_move || + @moveBlacklist.include?(GameData::Move.get(@copied_move).function_code) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbUseMoveSimple(@copied_move) + end +end + + + +#=============================================================================== +# Uses the move the target was about to use this round, with 1.5x power. +# (Me First) +#=============================================================================== +class PokeBattle_Move_0B0 < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + def callsAnotherMove?; return true; end + + def initialize(battle,move) + super + @moveBlacklist = [ + "0F1", # Covet, Thief + # Struggle, Chatter, Belch + "002", # Struggle + "014", # Chatter + "158", # Belch + # Counter moves + "071", # Counter + "072", # Mirror Coat + "073", # Metal Burst + # Moves that start focussing at the start of the round + "115", # Focus Punch + "171", # Shell Trap + "172" # Beak Blast + ] + end + + def pbFailsAgainstTarget?(user,target) + return true if pbMoveFailedTargetAlreadyMoved?(target) + oppMove = @battle.choices[target.index][2] + if !oppMove || oppMove.statusMove? || @moveBlacklist.include?(oppMove.function) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + user.effects[PBEffects::MeFirst] = true + user.pbUseMoveSimple(@battle.choices[target.index][2].id) + user.effects[PBEffects::MeFirst] = false + end +end + + + +#=============================================================================== +# This round, reflects all moves with the "C" flag targeting the user back at +# their origin. (Magic Coat) +#=============================================================================== +class PokeBattle_Move_0B1 < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::MagicCoat] = true + @battle.pbDisplay(_INTL("{1} shrouded itself with Magic Coat!",user.pbThis)) + end +end + + + +#=============================================================================== +# This round, snatches all used moves with the "D" flag. (Snatch) +#=============================================================================== +class PokeBattle_Move_0B2 < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::Snatch] = 1 + @battle.eachBattler do |b| + next if b.effects[PBEffects::Snatch]= 6 + @npMove = :ENERGYBALL if GameData::Move.exists?(:ENERGYBALL) + else + @npMove = :SEEDBOMB if GameData::Move.exists?(:SEEDBOMB) + end + when :MovingWater, :StillWater, :Underwater + @npMove = :HYDROPUMP if GameData::Move.exists?(:HYDROPUMP) + when :Puddle + @npMove = :MUDBOMB if GameData::Move.exists?(:MUDBOMB) + when :Cave + if Settings::MECHANICS_GENERATION >= 6 + @npMove = :POWERGEM if GameData::Move.exists?(:POWERGEM) + else + @npMove = :ROCKSLIDE if GameData::Move.exists?(:ROCKSLIDE) + end + when :Rock + if Settings::MECHANICS_GENERATION >= 6 + @npMove = :EARTHPOWER if GameData::Move.exists?(:EARTHPOWER) + else + @npMove = :ROCKSLIDE if GameData::Move.exists?(:ROCKSLIDE) + end + when :Sand + if Settings::MECHANICS_GENERATION >= 6 + @npMove = :EARTHPOWER if GameData::Move.exists?(:EARTHPOWER) + else + @npMove = :EARTHQUAKE if GameData::Move.exists?(:EARTHQUAKE) + end + when :Snow + if Settings::MECHANICS_GENERATION >= 6 + @npMove = :FROSTBREATH if GameData::Move.exists?(:FROSTBREATH) + else + @npMove = :BLIZZARD if GameData::Move.exists?(:BLIZZARD) + end + when :Ice + @npMove = :ICEBEAM if GameData::Move.exists?(:ICEBEAM) + when :Volcano + @npMove = :LAVAPLUME if GameData::Move.exists?(:LAVAPLUME) + when :Graveyard + @npMove = :SHADOWBALL if GameData::Move.exists?(:SHADOWBALL) + when :Sky + @npMove = :AIRSLASH if GameData::Move.exists?(:AIRSLASH) + when :Space + @npMove = :DRACOMETEOR if GameData::Move.exists?(:DRACOMETEOR) + when :UltraSpace + @npMove = :PSYSHOCK if GameData::Move.exists?(:PSYSHOCK) + end + end + end + + def pbEffectAgainstTarget(user,target) + @battle.pbDisplay(_INTL("{1} turned into {2}!", @name, GameData::Move.get(@npMove).name)) + user.pbUseMoveSimple(@npMove, target.index) + end +end + + + +#=============================================================================== +# Uses a random move the user knows. Fails if user is not asleep. (Sleep Talk) +#=============================================================================== +class PokeBattle_Move_0B4 < PokeBattle_Move + def usableWhenAsleep?; return true; end + def callsAnotherMove?; return true; end + + def initialize(battle,move) + super + @moveBlacklist = [ + "0D1", # Uproar + "0D4", # Bide + # Struggle, Chatter, Belch + "002", # Struggle # Not listed on Bulbapedia + "014", # Chatter # Not listed on Bulbapedia + "158", # Belch + # Moves that affect the moveset (except Transform) + "05C", # Mimic + "05D", # Sketch + # Moves that call other moves + "0AE", # Mirror Move + "0AF", # Copycat + "0B0", # Me First + "0B3", # Nature Power # Not listed on Bulbapedia + "0B4", # Sleep Talk + "0B5", # Assist + "0B6", # Metronome + # Two-turn attacks + "0C3", # Razor Wind + "0C4", # Solar Beam, Solar Blade + "0C5", # Freeze Shock + "0C6", # Ice Burn + "0C7", # Sky Attack + "0C8", # Skull Bash + "0C9", # Fly + "0CA", # Dig + "0CB", # Dive + "0CC", # Bounce + "0CD", # Shadow Force + "0CE", # Sky Drop + "12E", # Shadow Half + "14D", # Phantom Force + "14E", # Geomancy + # Moves that start focussing at the start of the round + "115", # Focus Punch + "171", # Shell Trap + "172" # Beak Blast + ] + end + + def pbMoveFailed?(user,targets) + @sleepTalkMoves = [] + user.eachMoveWithIndex do |m,i| + next if @moveBlacklist.include?(m.function) + next if !@battle.pbCanChooseMove?(user.index,i,false,true) + @sleepTalkMoves.push(i) + end + if !user.asleep? || @sleepTalkMoves.length==0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + choice = @sleepTalkMoves[@battle.pbRandom(@sleepTalkMoves.length)] + user.pbUseMoveSimple(user.moves[choice].id,user.pbDirectOpposing.index) + end +end + + + +#=============================================================================== +# Uses a random move known by any non-user Pokémon in the user's party. (Assist) +#=============================================================================== +class PokeBattle_Move_0B5 < PokeBattle_Move + def callsAnotherMove?; return true; end + + def initialize(battle,move) + super + @moveBlacklist = [ + # Struggle, Chatter, Belch + "002", # Struggle + "014", # Chatter + "158", # Belch + # Moves that affect the moveset + "05C", # Mimic + "05D", # Sketch + "069", # Transform + # Counter moves + "071", # Counter + "072", # Mirror Coat + "073", # Metal Burst # Not listed on Bulbapedia + # Helping Hand, Feint (always blacklisted together, don't know why) + "09C", # Helping Hand + "0AD", # Feint + # Protection moves + "0AA", # Detect, Protect + "0AB", # Quick Guard # Not listed on Bulbapedia + "0AC", # Wide Guard # Not listed on Bulbapedia + "0E8", # Endure + "149", # Mat Block + "14A", # Crafty Shield # Not listed on Bulbapedia + "14B", # King's Shield + "14C", # Spiky Shield + "168", # Baneful Bunker + # Moves that call other moves + "0AE", # Mirror Move + "0AF", # Copycat + "0B0", # Me First +# "0B3", # Nature Power # See below + "0B4", # Sleep Talk + "0B5", # Assist + "0B6", # Metronome + # Move-redirecting and stealing moves + "0B1", # Magic Coat # Not listed on Bulbapedia + "0B2", # Snatch + "117", # Follow Me, Rage Powder + "16A", # Spotlight + # Set up effects that trigger upon KO + "0E6", # Grudge # Not listed on Bulbapedia + "0E7", # Destiny Bond + # Target-switching moves +# "0EB", # Roar, Whirlwind # See below + "0EC", # Circle Throw, Dragon Tail + # Held item-moving moves + "0F1", # Covet, Thief + "0F2", # Switcheroo, Trick + "0F3", # Bestow + # Moves that start focussing at the start of the round + "115", # Focus Punch + "171", # Shell Trap + "172", # Beak Blast + # Event moves that do nothing + "133", # Hold Hands + "134" # Celebrate + ] + if Settings::MECHANICS_GENERATION >= 6 + @moveBlacklist += [ + # Moves that call other moves + "0B3", # Nature Power + # Two-turn attacks + "0C3", # Razor Wind # Not listed on Bulbapedia + "0C4", # Solar Beam, Solar Blade # Not listed on Bulbapedia + "0C5", # Freeze Shock # Not listed on Bulbapedia + "0C6", # Ice Burn # Not listed on Bulbapedia + "0C7", # Sky Attack # Not listed on Bulbapedia + "0C8", # Skull Bash # Not listed on Bulbapedia + "0C9", # Fly + "0CA", # Dig + "0CB", # Dive + "0CC", # Bounce + "0CD", # Shadow Force + "0CE", # Sky Drop + "12E", # Shadow Half + "14D", # Phantom Force + "14E", # Geomancy # Not listed on Bulbapedia + # Target-switching moves + "0EB" # Roar, Whirlwind + ] + end + end + + def pbMoveFailed?(user,targets) + @assistMoves = [] + # NOTE: This includes the Pokémon of ally trainers in multi battles. + @battle.pbParty(user.index).each_with_index do |pkmn,i| + next if !pkmn || i==user.pokemonIndex + next if Settings::MECHANICS_GENERATION >= 6 && pkmn.egg? + pkmn.moves.each do |move| + next if @moveBlacklist.include?(move.function_code) + next if move.type == :SHADOW + @assistMoves.push(move.id) + end + end + if @assistMoves.length==0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + move = @assistMoves[@battle.pbRandom(@assistMoves.length)] + user.pbUseMoveSimple(move) + end +end + + + +#=============================================================================== +# Uses a random move that exists. (Metronome) +#=============================================================================== +class PokeBattle_Move_0B6 < PokeBattle_Move + def callsAnotherMove?; return true; end + + def initialize(battle,move) + super + @moveBlacklist = [ + "011", # Snore + "11D", # After You + "11E", # Quash + "16C", # Instruct + # Struggle, Chatter, Belch + "002", # Struggle + "014", # Chatter + "158", # Belch + # Moves that affect the moveset + "05C", # Mimic + "05D", # Sketch + "069", # Transform + # Counter moves + "071", # Counter + "072", # Mirror Coat + "073", # Metal Burst # Not listed on Bulbapedia + # Helping Hand, Feint (always blacklisted together, don't know why) + "09C", # Helping Hand + "0AD", # Feint + # Protection moves + "0AA", # Detect, Protect + "0AB", # Quick Guard + "0AC", # Wide Guard + "0E8", # Endure + "149", # Mat Block + "14A", # Crafty Shield + "14B", # King's Shield + "14C", # Spiky Shield + "168", # Baneful Bunker + # Moves that call other moves + "0AE", # Mirror Move + "0AF", # Copycat + "0B0", # Me First + "0B3", # Nature Power + "0B4", # Sleep Talk + "0B5", # Assist + "0B6", # Metronome + # Move-redirecting and stealing moves + "0B1", # Magic Coat # Not listed on Bulbapedia + "0B2", # Snatch + "117", # Follow Me, Rage Powder + "16A", # Spotlight + # Set up effects that trigger upon KO + "0E6", # Grudge # Not listed on Bulbapedia + "0E7", # Destiny Bond + # Held item-moving moves + "0F1", # Covet, Thief + "0F2", # Switcheroo, Trick + "0F3", # Bestow + # Moves that start focussing at the start of the round + "115", # Focus Punch + "171", # Shell Trap + "172", # Beak Blast + # Event moves that do nothing + "133", # Hold Hands + "134" # Celebrate + ] + @moveBlacklistSignatures = [ + :SNARL, + # Signature moves + :DIAMONDSTORM, # Diancie (Gen 6) + :FLEURCANNON, # Magearna (Gen 7) + :FREEZESHOCK, # Black Kyurem (Gen 5) + :HYPERSPACEFURY, # Hoopa Unbound (Gen 6) + :HYPERSPACEHOLE, # Hoopa Confined (Gen 6) + :ICEBURN, # White Kyurem (Gen 5) + :LIGHTOFRUIN, # Eternal Flower Floette (Gen 6) + :MINDBLOWN, # Blacephalon (Gen 7) + :PHOTONGEYSER, # Necrozma (Gen 7) + :PLASMAFISTS, # Zeraora (Gen 7) + :RELICSONG, # Meloetta (Gen 5) + :SECRETSWORD, # Keldeo (Gen 5) + :SPECTRALTHIEF, # Marshadow (Gen 7) + :STEAMERUPTION, # Volcanion (Gen 6) + :TECHNOBLAST, # Genesect (Gen 5) + :THOUSANDARROWS, # Zygarde (Gen 6) + :THOUSANDWAVES, # Zygarde (Gen 6) + :VCREATE, # Victini (Gen 5) + :FAKEMOVE #not a real move + ] + end + + def pbMoveFailed?(user,targets) + @metronomeMove = nil + move_keys = GameData::Move::DATA.keys + # NOTE: You could be really unlucky and roll blacklisted moves 1000 times in + # a row. This is too unlikely to care about, though. + 1000.times do + move_id = move_keys[@battle.pbRandom(move_keys.length)] + move_data = GameData::Move.get(move_id) + next if @moveBlacklist.include?(move_data.function_code) + next if @moveBlacklistSignatures.include?(move_data.id) + next if move_data.type == :SHADOW + @metronomeMove = move_data.id + break + end + if !@metronomeMove + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbUseMoveSimple(@metronomeMove) + end +end + + + +#=============================================================================== +# The target can no longer use the same move twice in a row. (Torment) +#=============================================================================== +class PokeBattle_Move_0B7 < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::Torment] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedAromaVeil?(user,target) + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::Torment] = true + @battle.pbDisplay(_INTL("{1} was subjected to torment!",target.pbThis)) + target.pbItemStatusCureCheck + end +end + + + +#=============================================================================== +# Disables all target's moves that the user also knows. (Imprison) +#=============================================================================== +class PokeBattle_Move_0B8 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if user.effects[PBEffects::Imprison] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::Imprison] = true + @battle.pbDisplay(_INTL("{1} sealed any moves its target shares with it!",user.pbThis)) + end +end + + + +#=============================================================================== +# For 5 rounds, disables the last move the target used. (Disable) +#=============================================================================== +class PokeBattle_Move_0B9 < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::Disable]>0 || !target.lastRegularMoveUsed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedAromaVeil?(user,target) + canDisable = false + target.eachMove do |m| + next if m.id!=target.lastRegularMoveUsed + next if m.pp==0 && m.total_pp>0 + canDisable = true + break + end + if !canDisable + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::Disable] = 5 + target.effects[PBEffects::DisableMove] = target.lastRegularMoveUsed + @battle.pbDisplay(_INTL("{1}'s {2} was disabled!",target.pbThis, + GameData::Move.get(target.lastRegularMoveUsed).name)) + target.pbItemStatusCureCheck + end +end + + + +#=============================================================================== +# For 4 rounds, disables the target's non-damaging moves. (Taunt) +#=============================================================================== +class PokeBattle_Move_0BA < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::Taunt]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedAromaVeil?(user,target) + if Settings::MECHANICS_GENERATION >= 6 && target.hasActiveAbility?(:OBLIVIOUS) && + !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("But it failed!")) + else + @battle.pbDisplay(_INTL("But it failed because of {1}'s {2}!", + target.pbThis(true),target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::Taunt] = 4 + @battle.pbDisplay(_INTL("{1} fell for the taunt!",target.pbThis)) + target.pbItemStatusCureCheck + end +end + + + +#=============================================================================== +# For 5 rounds, disables the target's healing moves. (Heal Block) +#=============================================================================== +class PokeBattle_Move_0BB < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::HealBlock]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedAromaVeil?(user,target) + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::HealBlock] = 5 + @battle.pbDisplay(_INTL("{1} was prevented from healing!",target.pbThis)) + target.pbItemStatusCureCheck + end +end + + + +#=============================================================================== +# For 4 rounds, the target must use the same move each round. (Encore) +#=============================================================================== +class PokeBattle_Move_0BC < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def initialize(battle,move) + super + @moveBlacklist = [ + "0BC", # Encore + # Struggle + "002", # Struggle + # Moves that affect the moveset + "05C", # Mimic + "05D", # Sketch + "069", # Transform + # Moves that call other moves (see also below) + "0AE" # Mirror Move + ] + if Settings::MECHANICS_GENERATION >= 7 + @moveBlacklist += [ + # Moves that call other moves +# "0AE", # Mirror Move # See above + "0AF", # Copycat + "0B0", # Me First + "0B3", # Nature Power + "0B4", # Sleep Talk + "0B5", # Assist + "0B6" # Metronome + ] + end + end + + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::Encore]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if !target.lastRegularMoveUsed || + @moveBlacklist.include?(GameData::Move.get(target.lastRegularMoveUsed).function_code) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.effects[PBEffects::ShellTrap] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedAromaVeil?(user,target) + canEncore = false + target.eachMove do |m| + next if m.id!=target.lastRegularMoveUsed + next if m.pp==0 && m.total_pp>0 + canEncore = true + break + end + if !canEncore + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::Encore] = 4 + target.effects[PBEffects::EncoreMove] = target.lastRegularMoveUsed + @battle.pbDisplay(_INTL("{1} received an encore!",target.pbThis)) + target.pbItemStatusCureCheck + end +end + + + +#=============================================================================== +# Hits twice. +#=============================================================================== +class PokeBattle_Move_0BD < PokeBattle_Move + def multiHitMove?; return true; end + def pbNumHits(user,targets); return 2; end +end + + + +#=============================================================================== +# Hits twice. May poison the target on each hit. (Twineedle) +#=============================================================================== +class PokeBattle_Move_0BE < PokeBattle_PoisonMove + def multiHitMove?; return true; end + def pbNumHits(user,targets); return 2; end +end + + + +#=============================================================================== +# Hits 3 times. Power is multiplied by the hit number. (Triple Kick) +# An accuracy check is performed for each hit. +#=============================================================================== +class PokeBattle_Move_0BF < PokeBattle_Move + def multiHitMove?; return true; end + def pbNumHits(user,targets); return 3; end + + def successCheckPerHit? + return @accCheckPerHit + end + + def pbOnStartUse(user,targets) + @calcBaseDmg = 0 + @accCheckPerHit = !user.hasActiveAbility?(:SKILLLINK) + end + + def pbBaseDamage(baseDmg,user,target) + @calcBaseDmg += baseDmg + return @calcBaseDmg + end +end + + + +#=============================================================================== +# Hits 2-5 times. +#=============================================================================== +class PokeBattle_Move_0C0 < PokeBattle_Move + def multiHitMove?; return true; end + + def pbNumHits(user,targets) + if @id == :WATERSHURIKEN && user.isSpecies?(:GRENINJA) && user.form == 2 + return 3 + end + hitChances = [2,2,3,3,4,5] + r = @battle.pbRandom(hitChances.length) + r = hitChances.length-1 if user.hasActiveAbility?(:SKILLLINK) + return hitChances[r] + end + + def pbBaseDamage(baseDmg,user,target) + if @id == :WATERSHURIKEN && user.isSpecies?(:GRENINJA) && user.form == 2 + return 20 + end + return super + end +end + + + +#=============================================================================== +# Hits X times, where X is the number of non-user unfainted status-free Pokémon +# in the user's party (not including partner trainers). Fails if X is 0. +# Base power of each hit depends on the base Attack stat for the species of that +# hit's participant. (Beat Up) +#=============================================================================== +class PokeBattle_Move_0C1 < PokeBattle_Move + def multiHitMove?; return true; end + + def pbMoveFailed?(user,targets) + @beatUpList = [] + @battle.eachInTeamFromBattlerIndex(user.index) do |pkmn,i| + next if !pkmn.able? || pkmn.status != :NONE + @beatUpList.push(i) + end + if @beatUpList.length==0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbNumHits(user,targets) + return @beatUpList.length + end + + def pbBaseDamage(baseDmg,user,target) + i = @beatUpList.shift # First element in array, and removes it from array + atk = @battle.pbParty(user.index)[i].baseStats[:ATTACK] + return 5+(atk/10) + end +end + + + +#=============================================================================== +# Two turn attack. Attacks first turn, skips second turn (if successful). +#=============================================================================== +class PokeBattle_Move_0C2 < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::HyperBeam] = 2 + user.currentMove = @id + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Razor Wind) +#=============================================================================== +class PokeBattle_Move_0C3 < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} whipped up a whirlwind!",user.pbThis)) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Solar Beam, Solar Blade) +# Power halved in all weather except sunshine. In sunshine, takes 1 turn instead. +#=============================================================================== +class PokeBattle_Move_0C4 < PokeBattle_TwoTurnMove + def pbIsChargingTurn?(user) + ret = super + if !user.effects[PBEffects::TwoTurnAttack] + if [:Sun, :HarshSun].include?(@battle.pbWeather) + @powerHerb = false + @chargingTurn = true + @damagingTurn = true + return false + end + end + return ret + end + + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} took in sunlight!",user.pbThis)) + end + + def pbBaseDamageMultiplier(damageMult,user,target) + damageMult /= 2 if ![:None, :Sun, :HarshSun].include?(@battle.pbWeather) + return damageMult + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Freeze Shock) +# May paralyze the target. +#=============================================================================== +class PokeBattle_Move_0C5 < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} became cloaked in a freezing light!",user.pbThis)) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbParalyze(user) if target.pbCanParalyze?(user,false,self) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Ice Burn) +# May burn the target. +#=============================================================================== +class PokeBattle_Move_0C6 < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} became cloaked in freezing air!",user.pbThis)) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbBurn(user) if target.pbCanBurn?(user,false,self) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Sky Attack) +# May make the target flinch. +#=============================================================================== +class PokeBattle_Move_0C7 < PokeBattle_TwoTurnMove + def flinchingMove?; return true; end + + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} became cloaked in a harsh light!",user.pbThis)) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbFlinch(user) + end +end + + + +#=============================================================================== +# Two turn attack. Ups user's Defense by 1 stage first turn, attacks second turn. +# (Skull Bash) +#=============================================================================== +class PokeBattle_Move_0C8 < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} tucked in its head!",user.pbThis)) + end + + def pbChargingTurnEffect(user,target) + if user.pbCanRaiseStatStage?(:DEFENSE,user,self) + user.pbRaiseStatStage(:DEFENSE,1,user) + end + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Fly) +# (Handled in Battler's pbSuccessCheckPerHit): Is semi-invulnerable during use. +#=============================================================================== +class PokeBattle_Move_0C9 < PokeBattle_TwoTurnMove + def unusableInGravity?; return true; end + + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} flew up high!",user.pbThis)) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Dig) +# (Handled in Battler's pbSuccessCheckPerHit): Is semi-invulnerable during use. +#=============================================================================== +class PokeBattle_Move_0CA < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} burrowed its way under the ground!",user.pbThis)) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Dive) +# (Handled in Battler's pbSuccessCheckPerHit): Is semi-invulnerable during use. +#=============================================================================== +class PokeBattle_Move_0CB < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} hid underwater!",user.pbThis)) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Bounce) +# May paralyze the target. +# (Handled in Battler's pbSuccessCheckPerHit): Is semi-invulnerable during use. +#=============================================================================== +class PokeBattle_Move_0CC < PokeBattle_TwoTurnMove + def unusableInGravity?; return true; end + + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} sprang up!",user.pbThis)) + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbParalyze(user) if target.pbCanParalyze?(user,false,self) + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Shadow Force) +# Is invulnerable during use. Ends target's protections upon hit. +#=============================================================================== +class PokeBattle_Move_0CD < PokeBattle_TwoTurnMove + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} vanished instantly!",user.pbThis)) + end + + def pbAttackingTurnEffect(user,target) + target.effects[PBEffects::BanefulBunker] = false + target.effects[PBEffects::KingsShield] = false + target.effects[PBEffects::Protect] = false + target.effects[PBEffects::SpikyShield] = false + target.pbOwnSide.effects[PBEffects::CraftyShield] = false + target.pbOwnSide.effects[PBEffects::MatBlock] = false + target.pbOwnSide.effects[PBEffects::QuickGuard] = false + target.pbOwnSide.effects[PBEffects::WideGuard] = false + end +end + + + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Sky Drop) +# (Handled in Battler's pbSuccessCheckPerHit): Is semi-invulnerable during use. +# Target is also semi-invulnerable during use, and can't take any action. +# Doesn't damage airborne Pokémon (but still makes them unable to move during). +#=============================================================================== +class PokeBattle_Move_0CE < PokeBattle_TwoTurnMove + def unusableInGravity?; return true; end + + def pbIsChargingTurn?(user) + # NOTE: Sky Drop doesn't benefit from Power Herb, probably because it works + # differently (i.e. immobilises the target during use too). + @powerHerb = false + @chargingTurn = (user.effects[PBEffects::TwoTurnAttack].nil?) + @damagingTurn = (!user.effects[PBEffects::TwoTurnAttack].nil?) + return !@damagingTurn + end + + def pbFailsAgainstTarget?(user,target) + if !target.opposes?(user) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.effects[PBEffects::Substitute]>0 && !ignoresSubstitute?(user) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if Settings::MECHANICS_GENERATION >= 6 && target.pbWeight>=2000 # 200.0kg + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.semiInvulnerable? || + (target.effects[PBEffects::SkyDrop]>=0 && @chargingTurn) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.effects[PBEffects::SkyDrop]!=user.index && @damagingTurn + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbCalcTypeMod(movetype,user,target) + return Effectiveness::INEFFECTIVE if target.pbHasType?(:FLYING) + return super + end + + def pbChargingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} took {2} into the sky!",user.pbThis,targets[0].pbThis(true))) + end + + def pbAttackingTurnMessage(user,targets) + @battle.pbDisplay(_INTL("{1} was freed from the Sky Drop!",targets[0].pbThis)) + end + + def pbChargingTurnEffect(user,target) + target.effects[PBEffects::SkyDrop] = user.index + end + + def pbAttackingTurnEffect(user,target) + target.effects[PBEffects::SkyDrop] = -1 + end +end + + + +#=============================================================================== +# Trapping move. Traps for 5 or 6 rounds. Trapped Pokémon lose 1/16 of max HP +# at end of each round. +#=============================================================================== +class PokeBattle_Move_0CF < PokeBattle_Move + def pbEffectAgainstTarget(user,target) + return if target.fainted? || target.damageState.substitute + return if target.effects[PBEffects::Trapping]>0 + # Set trapping effect duration and info + if user.hasActiveItem?(:GRIPCLAW) + target.effects[PBEffects::Trapping] = (Settings::MECHANICS_GENERATION >= 5) ? 8 : 6 + else + target.effects[PBEffects::Trapping] = 5+@battle.pbRandom(2) + end + target.effects[PBEffects::TrappingMove] = @id + target.effects[PBEffects::TrappingUser] = user.index + # Message + msg = _INTL("{1} was trapped in the vortex!",target.pbThis) + case @id + when :BIND + msg = _INTL("{1} was squeezed by {2}!",target.pbThis,user.pbThis(true)) + when :CLAMP + msg = _INTL("{1} clamped {2}!",user.pbThis,target.pbThis(true)) + when :FIRESPIN + msg = _INTL("{1} was trapped in the fiery vortex!",target.pbThis) + when :INFESTATION + msg = _INTL("{1} has been afflicted with an infestation by {2}!",target.pbThis,user.pbThis(true)) + when :MAGMASTORM + msg = _INTL("{1} became trapped by Magma Storm!",target.pbThis) + when :SANDTOMB + msg = _INTL("{1} became trapped by Sand Tomb!",target.pbThis) + when :WHIRLPOOL + msg = _INTL("{1} became trapped in the vortex!",target.pbThis) + when :WRAP + msg = _INTL("{1} was wrapped by {2}!",target.pbThis,user.pbThis(true)) + end + @battle.pbDisplay(msg) + end +end + + + +#=============================================================================== +# Trapping move. Traps for 5 or 6 rounds. Trapped Pokémon lose 1/16 of max HP +# at end of each round. (Whirlpool) +# Power is doubled if target is using Dive. Hits some semi-invulnerable targets. +#=============================================================================== +class PokeBattle_Move_0D0 < PokeBattle_Move_0CF + def hitsDivingTargets?; return true; end + + def pbModifyDamage(damageMult,user,target) + damageMult *= 2 if target.inTwoTurnAttack?("0CB") # Dive + return damageMult + end +end + + + +#=============================================================================== +# User must use this move for 2 more rounds. No battlers can sleep. (Uproar) +# NOTE: Bulbapedia claims that an uproar will wake up Pokémon even if they have +# Soundproof, and will not allow Pokémon to fall asleep even if they have +# Soundproof. I think this is an oversight, so I've let Soundproof Pokémon +# be unaffected by Uproar waking/non-sleeping effects. +#=============================================================================== +class PokeBattle_Move_0D1 < PokeBattle_Move + def pbEffectGeneral(user) + return if user.effects[PBEffects::Uproar]>0 + user.effects[PBEffects::Uproar] = 3 + user.currentMove = @id + @battle.pbDisplay(_INTL("{1} caused an uproar!",user.pbThis)) + @battle.pbPriority(true).each do |b| + next if b.fainted? || b.status != :SLEEP + next if b.hasActiveAbility?(:SOUNDPROOF) + b.pbCureStatus + end + end +end + + + +#=============================================================================== +# User must use this move for 1 or 2 more rounds. At end, user becomes confused. +# (Outrage, Petal Dange, Thrash) +#=============================================================================== +class PokeBattle_Move_0D2 < PokeBattle_Move + def pbEffectAfterAllHits(user,target) + if !target.damageState.unaffected && user.effects[PBEffects::Outrage]==0 + user.effects[PBEffects::Outrage] = 2+@battle.pbRandom(2) + user.currentMove = @id + end + if user.effects[PBEffects::Outrage]>0 + user.effects[PBEffects::Outrage] -= 1 + if user.effects[PBEffects::Outrage]==0 && user.pbCanConfuseSelf?(false) + user.pbConfuse(_INTL("{1} became confused due to fatigue!",user.pbThis)) + end + end + end +end + + + +#=============================================================================== +# User must use this move for 4 more rounds. Power doubles each round. +# Power is also doubled if user has curled up. (Ice Ball, Rollout) +#=============================================================================== +class PokeBattle_Move_0D3 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + shift = (5 - user.effects[PBEffects::Rollout]) # 0-4, where 0 is most powerful + shift = 0 if user.effects[PBEffects::Rollout] == 0 # For first turn + shift += 1 if user.effects[PBEffects::DefenseCurl] + baseDmg *= 2**shift + return baseDmg + end + + def pbEffectAfterAllHits(user,target) + if !target.damageState.unaffected && user.effects[PBEffects::Rollout] == 0 + user.effects[PBEffects::Rollout] = 5 + user.currentMove = @id + end + user.effects[PBEffects::Rollout] -= 1 if user.effects[PBEffects::Rollout] > 0 + end +end + + + +#=============================================================================== +# User bides its time this round and next round. The round after, deals 2x the +# total direct damage it took while biding to the last battler that damaged it. +# (Bide) +#=============================================================================== +class PokeBattle_Move_0D4 < PokeBattle_FixedDamageMove + def pbAddTarget(targets,user) + return if user.effects[PBEffects::Bide]!=1 # Not the attack turn + idxTarget = user.effects[PBEffects::BideTarget] + t = (idxTarget>=0) ? @battle.battlers[idxTarget] : nil + if !user.pbAddTarget(targets,user,t,self,false) + user.pbAddTargetRandomFoe(targets,user,self,false) + end + end + + def pbMoveFailed?(user,targets) + return false if user.effects[PBEffects::Bide]!=1 # Not the attack turn + if user.effects[PBEffects::BideDamage]==0 + @battle.pbDisplay(_INTL("But it failed!")) + user.effects[PBEffects::Bide] = 0 # No need to reset other Bide variables + return true + end + if targets.length==0 + @battle.pbDisplay(_INTL("But there was no target...")) + user.effects[PBEffects::Bide] = 0 # No need to reset other Bide variables + return true + end + return false + end + + def pbOnStartUse(user,targets) + @damagingTurn = (user.effects[PBEffects::Bide]==1) # If attack turn + end + + def pbDisplayUseMessage(user) + if @damagingTurn # Attack turn + @battle.pbDisplayBrief(_INTL("{1} unleashed energy!",user.pbThis)) + elsif user.effects[PBEffects::Bide]>1 # Charging turns + @battle.pbDisplayBrief(_INTL("{1} is storing energy!",user.pbThis)) + else + super # Start using Bide + end + end + + def pbDamagingMove? # Stops damage being dealt in the charging turns + return false if !@damagingTurn + return super + end + + def pbFixedDamage(user,target) + return user.effects[PBEffects::BideDamage]*2 + end + + def pbEffectGeneral(user) + if user.effects[PBEffects::Bide]==0 # Starting using Bide + user.effects[PBEffects::Bide] = 3 + user.effects[PBEffects::BideDamage] = 0 + user.effects[PBEffects::BideTarget] = -1 + user.currentMove = @id + end + user.effects[PBEffects::Bide] -= 1 + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + hitNum = 1 if !@damagingTurn # Charging anim + super + end +end + + + +#=============================================================================== +# Heals user by 1/2 of its max HP. +#=============================================================================== +class PokeBattle_Move_0D5 < PokeBattle_HealingMove + def pbHealAmount(user) + return (user.totalhp/2.0).round + end +end + + + +#=============================================================================== +# Heals user by 1/2 of its max HP. (Roost) +# User roosts, and its Flying type is ignored for attacks used against it. +#=============================================================================== +class PokeBattle_Move_0D6 < PokeBattle_HealingMove + def pbHealAmount(user) + return (user.totalhp/2.0).round + end + + def pbEffectAfterAllHits(user,target) + user.effects[PBEffects::Roost] = true + end +end + + + +#=============================================================================== +# Battler in user's position is healed by 1/2 of its max HP, at the end of the +# next round. (Wish) +#=============================================================================== +class PokeBattle_Move_0D7 < PokeBattle_Move + def healingMove?; return true; end + + def pbMoveFailed?(user,targets) + if @battle.positions[user.index].effects[PBEffects::Wish]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.positions[user.index].effects[PBEffects::Wish] = 2 + @battle.positions[user.index].effects[PBEffects::WishAmount] = (user.totalhp/2.0).round + @battle.positions[user.index].effects[PBEffects::WishMaker] = user.pokemonIndex + end +end + + + +#=============================================================================== +# Heals user by an amount depending on the weather. (Moonlight, Morning Sun, +# Synthesis) +#=============================================================================== +class PokeBattle_Move_0D8 < PokeBattle_HealingMove + def pbOnStartUse(user,targets) + case @battle.pbWeather + when :Sun, :HarshSun + @healAmount = (user.totalhp*2/3.0).round + when :None, :StrongWinds + @healAmount = (user.totalhp/2.0).round + else + @healAmount = (user.totalhp/4.0).round + end + end + + def pbHealAmount(user) + return @healAmount + end +end + + + +#=============================================================================== +# Heals user to full HP. User falls asleep for 2 more rounds. (Rest) +#=============================================================================== +class PokeBattle_Move_0D9 < PokeBattle_HealingMove + def pbMoveFailed?(user,targets) + if user.asleep? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if !user.pbCanSleep?(user,true,self,true) + return true if super + return false + end + + def pbHealAmount(user) + return user.totalhp-user.hp + end + + def pbEffectGeneral(user) + user.pbSleepSelf(_INTL("{1} slept and became healthy!",user.pbThis),3) + super + end +end + + + +#=============================================================================== +# Rings the user. Ringed Pokémon gain 1/16 of max HP at the end of each round. +# (Aqua Ring) +#=============================================================================== +class PokeBattle_Move_0DA < PokeBattle_Move + def pbMoveFailed?(user,targets) + if user.effects[PBEffects::AquaRing] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::AquaRing] = true + @battle.pbDisplay(_INTL("{1} surrounded itself with a veil of water!",user.pbThis)) + end +end + + + +#=============================================================================== +# Ingrains the user. Ingrained Pokémon gain 1/16 of max HP at the end of each +# round, and cannot flee or switch out. (Ingrain) +#=============================================================================== +class PokeBattle_Move_0DB < PokeBattle_Move + def pbMoveFailed?(user,targets) + if user.effects[PBEffects::Ingrain] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::Ingrain] = true + @battle.pbDisplay(_INTL("{1} planted its roots!",user.pbThis)) + end +end + + + +#=============================================================================== +# Seeds the target. Seeded Pokémon lose 1/8 of max HP at the end of each round, +# and the Pokémon in the user's position gains the same amount. (Leech Seed) +#=============================================================================== +class PokeBattle_Move_0DC < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::LeechSeed]>=0 + @battle.pbDisplay(_INTL("{1} evaded the attack!",target.pbThis)) + return true + end + if target.pbHasType?(:GRASS) + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + return true + end + return false + end + + def pbMissMessage(user,target) + @battle.pbDisplay(_INTL("{1} evaded the attack!",target.pbThis)) + return true + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::LeechSeed] = user.index + @battle.pbDisplay(_INTL("{1} was seeded!",target.pbThis)) + end +end + + + +#=============================================================================== +# User gains half the HP it inflicts as damage. +#=============================================================================== +class PokeBattle_Move_0DD < PokeBattle_Move + def healingMove?; return Settings::MECHANICS_GENERATION >= 6; end + + def pbEffectAgainstTarget(user,target) + return if target.damageState.hpLost<=0 + hpGain = (target.damageState.hpLost/2.0).round + user.pbRecoverHPFromDrain(hpGain,target) + end +end + + + +#=============================================================================== +# User gains half the HP it inflicts as damage. Fails if target is not asleep. +# (Dream Eater) +#=============================================================================== +class PokeBattle_Move_0DE < PokeBattle_Move + def healingMove?; return Settings::MECHANICS_GENERATION >= 6; end + + def pbFailsAgainstTarget?(user,target) + if !target.asleep? + @battle.pbDisplay(_INTL("{1} wasn't affected!",target.pbThis)) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + return if target.damageState.hpLost<=0 + hpGain = (target.damageState.hpLost/2.0).round + user.pbRecoverHPFromDrain(hpGain,target) + end +end + + + +#=============================================================================== +# Heals target by 1/2 of its max HP. (Heal Pulse) +#=============================================================================== +class PokeBattle_Move_0DF < PokeBattle_Move + def healingMove?; return true; end + + def pbFailsAgainstTarget?(user,target) + if target.hp==target.totalhp + @battle.pbDisplay(_INTL("{1}'s HP is full!",target.pbThis)) + return true + elsif !target.canHeal? + @battle.pbDisplay(_INTL("{1} is unaffected!",target.pbThis)) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + hpGain = (target.totalhp/2.0).round + if pulseMove? && user.hasActiveAbility?(:MEGALAUNCHER) + hpGain = (target.totalhp*3/4.0).round + end + target.pbRecoverHP(hpGain) + @battle.pbDisplay(_INTL("{1}'s HP was restored.",target.pbThis)) + end +end + + + +#=============================================================================== +# User faints, even if the move does nothing else. (Explosion, Self-Destruct) +#=============================================================================== +class PokeBattle_Move_0E0 < PokeBattle_Move + def worksWithNoTargets?; return true; end + def pbNumHits(user,targets); return 1; end + + def pbMoveFailed?(user,targets) + if !@battle.moldBreaker + bearer = @battle.pbCheckGlobalAbility(:DAMP) + if bearer!=nil + @battle.pbShowAbilitySplash(bearer) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} cannot use {2}!",user.pbThis,@name)) + else + @battle.pbDisplay(_INTL("{1} cannot use {2} because of {3}'s {4}!", + user.pbThis,@name,bearer.pbThis(true),bearer.abilityName)) + end + @battle.pbHideAbilitySplash(bearer) + return true + end + end + return false + end + + def pbSelfKO(user) + return if user.fainted? + user.pbReduceHP(user.hp,false) + user.pbItemHPHealCheck + end +end + + + +#=============================================================================== +# Inflicts fixed damage equal to user's current HP. (Final Gambit) +# User faints (if successful). +#=============================================================================== +class PokeBattle_Move_0E1 < PokeBattle_FixedDamageMove + def pbNumHits(user,targets); return 1; end + + def pbOnStartUse(user,targets) + @finalGambitDamage = user.hp + end + + def pbFixedDamage(user,target) + return @finalGambitDamage + end + + def pbSelfKO(user) + return if user.fainted? + user.pbReduceHP(user.hp,false) + user.pbItemHPHealCheck + end +end + + + +#=============================================================================== +# Decreases the target's Attack and Special Attack by 2 stages each. (Memento) +# User faints (if successful). +#=============================================================================== +class PokeBattle_Move_0E2 < PokeBattle_TargetMultiStatDownMove + def initialize(battle,move) + super + @statDown = [:ATTACK,2,:SPECIAL_ATTACK,2] + end + + # NOTE: The user faints even if the target's stats cannot be changed, so this + # method must always return false to allow the move's usage to continue. + def pbFailsAgainstTarget?(user,target) + return false + end + + def pbSelfKO(user) + return if user.fainted? + user.pbReduceHP(user.hp,false) + user.pbItemHPHealCheck + end +end + + + +#=============================================================================== +# User faints. The Pokémon that replaces the user is fully healed (HP and +# status). Fails if user won't be replaced. (Healing Wish) +#=============================================================================== +class PokeBattle_Move_0E3 < PokeBattle_Move + def healingMove?; return true; end + + def pbMoveFailed?(user,targets) + if !@battle.pbCanChooseNonActive?(user.index) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbSelfKO(user) + return if user.fainted? + user.pbReduceHP(user.hp,false) + user.pbItemHPHealCheck + @battle.positions[user.index].effects[PBEffects::HealingWish] = true + end +end + + + +#=============================================================================== +# User faints. The Pokémon that replaces the user is fully healed (HP, PP and +# status). Fails if user won't be replaced. (Lunar Dance) +#=============================================================================== +class PokeBattle_Move_0E4 < PokeBattle_Move + def healingMove?; return true; end + + def pbMoveFailed?(user,targets) + if !@battle.pbCanChooseNonActive?(user.index) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbSelfKO(user) + return if user.fainted? + user.pbReduceHP(user.hp,false) + user.pbItemHPHealCheck + @battle.positions[user.index].effects[PBEffects::LunarDance] = true + end +end + + + +#=============================================================================== +# All current battlers will perish after 3 more rounds. (Perish Song) +#=============================================================================== +class PokeBattle_Move_0E5 < PokeBattle_Move + def pbMoveFailed?(user,targets) + failed = true + targets.each do |b| + next if b.effects[PBEffects::PerishSong]>0 # Heard it before + failed = false + break + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user,target) + return target.effects[PBEffects::PerishSong]>0 # Heard it before + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::PerishSong] = 4 + target.effects[PBEffects::PerishSongUser] = user.index + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + super + @battle.pbDisplay(_INTL("All Pokémon that hear the song will faint in three turns!")) + end +end + + + +#=============================================================================== +# If user is KO'd before it next moves, the attack that caused it loses all PP. +# (Grudge) +#=============================================================================== +class PokeBattle_Move_0E6 < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::Grudge] = true + @battle.pbDisplay(_INTL("{1} wants its target to bear a grudge!",user.pbThis)) + end +end + + + +#=============================================================================== +# If user is KO'd before it next moves, the battler that caused it also faints. +# (Destiny Bond) +#=============================================================================== +class PokeBattle_Move_0E7 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if Settings::MECHANICS_GENERATION >= 7 && user.effects[PBEffects::DestinyBondPrevious] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::DestinyBond] = true + @battle.pbDisplay(_INTL("{1} is hoping to take its attacker down with it!",user.pbThis)) + end +end + + + +#=============================================================================== +# If user would be KO'd this round, it survives with 1HP instead. (Endure) +#=============================================================================== +class PokeBattle_Move_0E8 < PokeBattle_ProtectMove + def initialize(battle,move) + super + @effect = PBEffects::Endure + end + + def pbProtectMessage(user) + @battle.pbDisplay(_INTL("{1} braced itself!",user.pbThis)) + end +end + + + +#=============================================================================== +# If target would be KO'd by this attack, it survives with 1HP instead. +# (False Swipe, Hold Back) +#=============================================================================== +class PokeBattle_Move_0E9 < PokeBattle_Move + def nonLethal?(user,target); return true; end +end + + + +#=============================================================================== +# User flees from battle. Fails in trainer battles. (Teleport) +#=============================================================================== +class PokeBattle_Move_0EA < PokeBattle_Move + def pbMoveFailed?(user,targets) + if !@battle.pbCanRun?(user.index) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.pbDisplay(_INTL("{1} fled from battle!",user.pbThis)) + @battle.decision = 3 # Escaped + end +end + + + +#=============================================================================== +# In wild battles, makes target flee. Fails if target is a higher level than the +# user. +# In trainer battles, target switches out. +# For status moves. (Roar, Whirlwind) +#=============================================================================== +class PokeBattle_Move_0EB < PokeBattle_Move + def ignoresSubstitute?(user); return true; end + + def pbFailsAgainstTarget?(user,target) + if target.hasActiveAbility?(:SUCTIONCUPS) && !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} anchors itself!",target.pbThis)) + else + @battle.pbDisplay(_INTL("{1} anchors itself with {2}!",target.pbThis,target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return true + end + if target.effects[PBEffects::Ingrain] + @battle.pbDisplay(_INTL("{1} anchored itself with its roots!",target.pbThis)) + return true + end + if !@battle.canRun + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + # if @battle.wildBattle? && target.level>user.level + # @battle.pbDisplay(_INTL("But it failed!")) + # return true + # end + if @battle.trainerBattle? + canSwitch = false + @battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn,i| + next if !@battle.pbCanSwitchLax?(target.index,i) + canSwitch = true + break + end + if !canSwitch + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + end + return false + end + + def pbEffectGeneral(user) + @battle.decision = 3 if @battle.wildBattle? # Escaped from battle + end + + def pbSwitchOutTargetsEffect(user,targets,numHits,switchedBattlers) + return if @battle.wildBattle? + return if user.fainted? || numHits==0 + roarSwitched = [] + targets.each do |b| + next if b.fainted? || b.damageState.unaffected || switchedBattlers.include?(b.index) + newPkmn = @battle.pbGetReplacementPokemonIndex(b.index,true) # Random + next if newPkmn<0 + @battle.pbRecallAndReplace(b.index, newPkmn, true) + @battle.pbDisplay(_INTL("{1} was dragged out!",b.pbThis)) + @battle.pbClearChoice(b.index) # Replacement Pokémon does nothing this round + switchedBattlers.push(b.index) + roarSwitched.push(b.index) + end + if roarSwitched.length>0 + @battle.moldBreaker = false if roarSwitched.include?(user.index) + @battle.pbPriority(true).each do |b| + b.pbEffectsOnSwitchIn(true) if roarSwitched.include?(b.index) + end + end + end +end + + + +#=============================================================================== +# In wild battles, makes target flee. Fails if target is a higher level than the +# user. +# In trainer battles, target switches out. +# For damaging moves. (Circle Throw, Dragon Tail) +#=============================================================================== +class PokeBattle_Move_0EC < PokeBattle_Move + def pbEffectAgainstTarget(user,target) + if @battle.wildBattle? && target.level<=user.level && @battle.canRun && + (target.effects[PBEffects::Substitute]==0 || ignoresSubstitute?(user)) + @battle.decision = 3 + end + end + + def pbSwitchOutTargetsEffect(user,targets,numHits,switchedBattlers) + return if @battle.wildBattle? + return if user.fainted? || numHits==0 + roarSwitched = [] + targets.each do |b| + next if b.fainted? || b.damageState.unaffected || b.damageState.substitute + next if switchedBattlers.include?(b.index) + next if b.effects[PBEffects::Ingrain] + next if b.hasActiveAbility?(:SUCTIONCUPS) && !@battle.moldBreaker + newPkmn = @battle.pbGetReplacementPokemonIndex(b.index,true) # Random + next if newPkmn<0 + @battle.pbRecallAndReplace(b.index, newPkmn, true) + @battle.pbDisplay(_INTL("{1} was dragged out!",b.pbThis)) + @battle.pbClearChoice(b.index) # Replacement Pokémon does nothing this round + switchedBattlers.push(b.index) + roarSwitched.push(b.index) + end + if roarSwitched.length>0 + @battle.moldBreaker = false if roarSwitched.include?(user.index) + @battle.pbPriority(true).each do |b| + b.pbEffectsOnSwitchIn(true) if roarSwitched.include?(b.index) + end + end + end +end + + + +#=============================================================================== +# User switches out. Various effects affecting the user are passed to the +# replacement. (Baton Pass) +#=============================================================================== +class PokeBattle_Move_0ED < PokeBattle_Move + def pbMoveFailed?(user,targets) + if !@battle.pbCanChooseNonActive?(user.index) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEndOfMoveUsageEffect(user,targets,numHits,switchedBattlers) + return if user.fainted? || numHits==0 + return if !@battle.pbCanChooseNonActive?(user.index) + @battle.pbPursuit(user.index) + return if user.fainted? + newPkmn = @battle.pbGetReplacementPokemonIndex(user.index) # Owner chooses + return if newPkmn<0 + @battle.pbRecallAndReplace(user.index, newPkmn, false, true) + @battle.pbClearChoice(user.index) # Replacement Pokémon does nothing this round + @battle.moldBreaker = false + switchedBattlers.push(user.index) + user.pbEffectsOnSwitchIn(true) + end +end + + + +#=============================================================================== +# After inflicting damage, user switches out. Ignores trapping moves. +# (U-turn, Volt Switch) +#=============================================================================== +class PokeBattle_Move_0EE < PokeBattle_Move + def pbEndOfMoveUsageEffect(user,targets,numHits,switchedBattlers) + return if user.fainted? || numHits==0 + targetSwitched = true + targets.each do |b| + targetSwitched = false if !switchedBattlers.include?(b.index) + end + return if targetSwitched + return if !@battle.pbCanChooseNonActive?(user.index) + @battle.pbDisplay(_INTL("{1} went back to {2}!",user.pbThis, + @battle.pbGetOwnerName(user.index))) + @battle.pbPursuit(user.index) + return if user.fainted? + newPkmn = @battle.pbGetReplacementPokemonIndex(user.index) # Owner chooses + return if newPkmn<0 + @battle.pbRecallAndReplace(user.index,newPkmn) + @battle.pbClearChoice(user.index) # Replacement Pokémon does nothing this round + @battle.moldBreaker = false + switchedBattlers.push(user.index) + user.pbEffectsOnSwitchIn(true) + end +end + + + +#=============================================================================== +# Target can no longer switch out or flee, as long as the user remains active. +# (Anchor Shot, Block, Mean Look, Spider Web, Spirit Shackle, Thousand Waves) +#=============================================================================== +class PokeBattle_Move_0EF < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + return false if damagingMove? + if target.effects[PBEffects::MeanLook]>=0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if Settings::MORE_TYPE_EFFECTS && target.pbHasType?(:GHOST) + @battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + return if damagingMove? + target.effects[PBEffects::MeanLook] = user.index + @battle.pbDisplay(_INTL("{1} can no longer escape!",target.pbThis)) + end + + def pbAdditionalEffect(user,target) + return if target.fainted? || target.damageState.substitute + return if target.effects[PBEffects::MeanLook]>=0 + return if Settings::MORE_TYPE_EFFECTS && target.pbHasType?(:GHOST) + target.effects[PBEffects::MeanLook] = user.index + @battle.pbDisplay(_INTL("{1} can no longer escape!",target.pbThis)) + end +end + + + +#=============================================================================== +# Target drops its item. It regains the item at the end of the battle. (Knock Off) +# If target has a losable item, damage is multiplied by 1.5. +#=============================================================================== +class PokeBattle_Move_0F0 < PokeBattle_Move + def pbBaseDamage(baseDmg,user,target) + if Settings::MECHANICS_GENERATION >= 6 && + target.item && !target.unlosableItem?(target.item) + # NOTE: Damage is still boosted even if target has Sticky Hold or a + # substitute. + baseDmg = (baseDmg*1.5).round + end + return baseDmg + end + + def pbEffectAfterAllHits(user,target) + return if @battle.wildBattle? && user.opposes? # Wild Pokémon can't knock off + return if user.fainted? + return if target.damageState.unaffected || target.damageState.substitute + return if !target.item || target.unlosableItem?(target.item) + return if target.hasActiveAbility?(:STICKYHOLD) && !@battle.moldBreaker + itemName = target.itemName + target.pbRemoveItem(false) + @battle.pbDisplay(_INTL("{1} dropped its {2}!",target.pbThis,itemName)) + end +end + + + +#=============================================================================== +# User steals the target's item, if the user has none itself. (Covet, Thief) +# Items stolen from wild Pokémon are kept after the battle. +#=============================================================================== +class PokeBattle_Move_0F1 < PokeBattle_Move + def pbEffectAfterAllHits(user,target) + return if @battle.wildBattle? && user.opposes? # Wild Pokémon can't thieve + return if user.fainted? + return if target.damageState.unaffected || target.damageState.substitute + return if !target.item || user.item + return if target.unlosableItem?(target.item) + return if user.unlosableItem?(target.item) + return if target.hasActiveAbility?(:STICKYHOLD) && !@battle.moldBreaker + itemName = target.itemName + user.item = target.item + # Permanently steal the item from wild Pokémon + # removed target.item == target.initialItem, this may cause bugs. + if @battle.wildBattle? && target.opposes? && !user.initialItem + user.setInitialItem(target.item) + end + target.pbRemoveItem(false) + @battle.pbDisplay(_INTL("{1} stole {2}'s {3}!",user.pbThis,target.pbThis(true),itemName)) + user.pbHeldItemTriggerCheck + end +end + + + +#=============================================================================== +# User and target swap items. They remain swapped after wild battles. +# (Switcheroo, Trick) +#=============================================================================== +class PokeBattle_Move_0F2 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if @battle.wildBattle? && user.opposes? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user,target) + if !user.item && !target.item + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.unlosableItem?(target.item) || + target.unlosableItem?(user.item) || + user.unlosableItem?(user.item) || + user.unlosableItem?(target.item) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.hasActiveAbility?(:STICKYHOLD) && !@battle.moldBreaker + @battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("But it failed to affect {1}!",target.pbThis(true))) + else + @battle.pbDisplay(_INTL("But it failed to affect {1} because of its {2}!", + target.pbThis(true),target.abilityName)) + end + @battle.pbHideAbilitySplash(target) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + oldUserItem = user.item; oldUserItemName = user.itemName + oldTargetItem = target.item; oldTargetItemName = target.itemName + user.item = oldTargetItem + user.effects[PBEffects::ChoiceBand] = nil + user.effects[PBEffects::Unburden] = (!user.item && oldUserItem) + target.item = oldUserItem + target.effects[PBEffects::ChoiceBand] = nil + target.effects[PBEffects::Unburden] = (!target.item && oldTargetItem) + # Permanently steal the item from wild Pokémon + if @battle.wildBattle? && target.opposes? && + target.initialItem==oldTargetItem && !user.initialItem + user.setInitialItem(oldTargetItem) + end + @battle.pbDisplay(_INTL("{1} switched items with its opponent!",user.pbThis)) + @battle.pbDisplay(_INTL("{1} obtained {2}.",user.pbThis,oldTargetItemName)) if oldTargetItem + @battle.pbDisplay(_INTL("{1} obtained {2}.",target.pbThis,oldUserItemName)) if oldUserItem + user.pbHeldItemTriggerCheck + target.pbHeldItemTriggerCheck + end +end + + + +#=============================================================================== +# User gives its item to the target. The item remains given after wild battles. +# (Bestow) +#=============================================================================== +class PokeBattle_Move_0F3 < PokeBattle_Move + def ignoresSubstitute?(user) + return true if Settings::MECHANICS_GENERATION >= 6 + return super + end + + def pbMoveFailed?(user,targets) + if !user.item || user.unlosableItem?(user.item) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user,target) + if target.item || target.unlosableItem?(user.item) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + itemName = user.itemName + target.item = user.item + # Permanently steal the item from wild Pokémon + if @battle.wildBattle? && user.opposes? && + user.initialItem==user.item && !target.initialItem + target.setInitialItem(user.item) + user.pbRemoveItem + else + user.pbRemoveItem(false) + end + @battle.pbDisplay(_INTL("{1} received {2} from {3}!",target.pbThis,itemName,user.pbThis(true))) + target.pbHeldItemTriggerCheck + end +end + + + +#=============================================================================== +# User consumes target's berry and gains its effect. (Bug Bite, Pluck) +#=============================================================================== +class PokeBattle_Move_0F4 < PokeBattle_Move + def pbEffectAfterAllHits(user,target) + return if user.fainted? || target.fainted? + return if target.damageState.unaffected || target.damageState.substitute + return if !target.item || !target.item.is_berry? + return if target.hasActiveAbility?(:STICKYHOLD) && !@battle.moldBreaker + item = target.item + itemName = target.itemName + target.pbRemoveItem + @battle.pbDisplay(_INTL("{1} stole and ate its target's {2}!",user.pbThis,itemName)) + user.pbHeldItemTriggerCheck(item,false) + end +end + + + +#=============================================================================== +# Target's berry/Gem is destroyed. (Incinerate) +#=============================================================================== +class PokeBattle_Move_0F5 < PokeBattle_Move + def pbEffectWhenDealingDamage(user,target) + return if target.damageState.substitute || target.damageState.berryWeakened + return if !target.item || (!target.item.is_berry? && + !(Settings::MECHANICS_GENERATION >= 6 && target.item.is_gem?)) + target.pbRemoveItem + @battle.pbDisplay(_INTL("{1}'s {2} was incinerated!",target.pbThis,target.itemName)) + end +end + + + +#=============================================================================== +# User recovers the last item it held and consumed. (Recycle) +#=============================================================================== +class PokeBattle_Move_0F6 < PokeBattle_Move + def pbMoveFailed?(user,targets) + if !user.recycleItem + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + item = user.recycleItem + user.item = item + user.setInitialItem(item) if @battle.wildBattle? && !user.initialItem + user.setRecycleItem(nil) + user.effects[PBEffects::PickupItem] = nil + user.effects[PBEffects::PickupUse] = 0 + itemName = GameData::Item.get(item).name + if itemName.starts_with_vowel? + @battle.pbDisplay(_INTL("{1} found an {2}!",user.pbThis,itemName)) + else + @battle.pbDisplay(_INTL("{1} found a {2}!",user.pbThis,itemName)) + end + user.pbHeldItemTriggerCheck + end +end + + + +#=============================================================================== +# User flings its item at the target. Power/effect depend on the item. (Fling) +#=============================================================================== +class PokeBattle_Move_0F7 < PokeBattle_Move + def initialize(battle,move) + super + # 80 => all Mega Stones + # 10 => all Berries + @flingPowers = { + 130 => [:IRONBALL + ], + 100 => [:HARDSTONE,:RAREBONE, + # Fossils + :ARMORFOSSIL,:CLAWFOSSIL,:COVERFOSSIL,:DOMEFOSSIL,:HELIXFOSSIL, + :JAWFOSSIL,:OLDAMBER,:PLUMEFOSSIL,:ROOTFOSSIL,:SAILFOSSIL, + :SKULLFOSSIL + ], + 90 => [:DEEPSEATOOTH,:GRIPCLAW,:THICKCLUB, + # Plates + :DRACOPLATE,:DREADPLATE,:EARTHPLATE,:FISTPLATE,:FLAMEPLATE, + :ICICLEPLATE,:INSECTPLATE,:IRONPLATE,:MEADOWPLATE,:MINDPLATE, + :PIXIEPLATE,:SKYPLATE,:SPLASHPLATE,:SPOOKYPLATE,:STONEPLATE, + :TOXICPLATE,:ZAPPLATE + ], + 80 => [:ASSAULTVEST,:DAWNSTONE,:DUSKSTONE,:ELECTIRIZER,:MAGMARIZER, + :ODDKEYSTONE,:OVALSTONE,:PROTECTOR,:QUICKCLAW,:RAZORCLAW,:SACHET, + :SAFETYGOGGLES,:SHINYSTONE,:STICKYBARB,:WEAKNESSPOLICY, + :WHIPPEDDREAM + ], + 70 => [:DRAGONFANG,:POISONBARB, + # EV-training items (Macho Brace is 60) + :POWERANKLET,:POWERBAND,:POWERBELT,:POWERBRACER,:POWERLENS, + :POWERWEIGHT, + # Drives + :BURNDRIVE,:CHILLDRIVE,:DOUSEDRIVE,:SHOCKDRIVE + ], + 60 => [:ADAMANTORB,:DAMPROCK,:GRISEOUSORB,:HEATROCK,:LUSTROUSORB, + :MACHOBRACE,:ROCKYHELMET,:STICK,:TERRAINEXTENDER + ], + 50 => [:DUBIOUSDISC,:SHARPBEAK, + # Memories + :BUGMEMORY,:DARKMEMORY,:DRAGONMEMORY,:ELECTRICMEMORY,:FAIRYMEMORY, + :FIGHTINGMEMORY,:FIREMEMORY,:FLYINGMEMORY,:GHOSTMEMORY, + :GRASSMEMORY,:GROUNDMEMORY,:ICEMEMORY,:POISONMEMORY, + :PSYCHICMEMORY,:ROCKMEMORY,:STEELMEMORY,:WATERMEMORY + ], + 40 => [:EVIOLITE,:ICYROCK,:LUCKYPUNCH + ], + 30 => [:ABSORBBULB,:ADRENALINEORB,:AMULETCOIN,:BINDINGBAND,:BLACKBELT, + :BLACKGLASSES,:BLACKSLUDGE,:BOTTLECAP,:CELLBATTERY,:CHARCOAL, + :CLEANSETAG,:DEEPSEASCALE,:DRAGONSCALE,:EJECTBUTTON,:ESCAPEROPE, + :EXPSHARE,:FLAMEORB,:FLOATSTONE,:FLUFFYTAIL,:GOLDBOTTLECAP, + :HEARTSCALE,:HONEY,:KINGSROCK,:LIFEORB,:LIGHTBALL,:LIGHTCLAY, + :LUCKYEGG,:LUMINOUSMOSS,:MAGNET,:METALCOAT,:METRONOME, + :MIRACLESEED,:MYSTICWATER,:NEVERMELTICE,:PASSORB,:POKEDOLL, + :POKETOY,:PRISMSCALE,:PROTECTIVEPADS,:RAZORFANG,:SACREDASH, + :SCOPELENS,:SHELLBELL,:SHOALSALT,:SHOALSHELL,:SMOKEBALL,:SNOWBALL, + :SOULDEW,:SPELLTAG,:TOXICORB,:TWISTEDSPOON,:UPGRADE, + # Healing items + :ANTIDOTE,:AWAKENING,:BERRYJUICE,:BIGMALASADA,:BLUEFLUTE, + :BURNHEAL,:CASTELIACONE,:ELIXIR,:ENERGYPOWDER,:ENERGYROOT,:ETHER, + :FRESHWATER,:FULLHEAL,:FULLRESTORE,:HEALPOWDER,:HYPERPOTION, + :ICEHEAL,:LAVACOOKIE,:LEMONADE,:LUMIOSEGALETTE,:MAXELIXIR, + :MAXETHER,:MAXPOTION,:MAXREVIVE,:MOOMOOMILK,:OLDGATEAU, + :PARALYZEHEAL,:PARLYZHEAL,:PEWTERCRUNCHIES,:POTION,:RAGECANDYBAR, + :REDFLUTE,:REVIVALHERB,:REVIVE,:SHALOURSABLE,:SODAPOP, + :SUPERPOTION,:SWEETHEART,:YELLOWFLUTE, + # Battle items + :XACCURACY,:XACCURACY2,:XACCURACY3,:XACCURACY6, + :XATTACK,:XATTACK2,:XATTACK3,:XATTACK6, + :XDEFEND,:XDEFEND2,:XDEFEND3,:XDEFEND6, + :XDEFENSE,:XDEFENSE2,:XDEFENSE3,:XDEFENSE6, + :XSPATK,:XSPATK2,:XSPATK3,:XSPATK6, + :XSPECIAL,:XSPECIAL2,:XSPECIAL3,:XSPECIAL6, + :XSPDEF,:XSPDEF2,:XSPDEF3,:XSPDEF6, + :XSPEED,:XSPEED2,:XSPEED3,:XSPEED6, + :DIREHIT,:DIREHIT2,:DIREHIT3, + :ABILITYURGE,:GUARDSPEC,:ITEMDROP,:ITEMURGE,:RESETURGE, + # Vitamins + :CALCIUM,:CARBOS,:HPUP,:IRON,:PPUP,:PPMAX,:PROTEIN,:ZINC, + :RARECANDY, + # Most evolution stones (see also 80) + :EVERSTONE,:FIRESTONE,:ICESTONE,:LEAFSTONE,:MOONSTONE,:SUNSTONE, + :THUNDERSTONE,:WATERSTONE, + # Repels + :MAXREPEL,:REPEL,:SUPERREPEL, + # Mulches + :AMAZEMULCH,:BOOSTMULCH,:DAMPMULCH,:GOOEYMULCH,:GROWTHMULCH, + :RICHMULCH,:STABLEMULCH,:SURPRISEMULCH, + # Shards + :BLUESHARD,:GREENSHARD,:REDSHARD,:YELLOWSHARD, + # Valuables + :BALMMUSHROOM,:BIGMUSHROOM,:BIGNUGGET,:BIGPEARL,:COMETSHARD, + :NUGGET,:PEARL,:PEARLSTRING,:RELICBAND,:RELICCOPPER,:RELICCROWN, + :RELICGOLD,:RELICSILVER,:RELICSTATUE,:RELICVASE,:STARDUST, + :STARPIECE,:STRANGESOUVENIR,:TINYMUSHROOM + ], + 20 => [# Wings + :CLEVERWING,:GENIUSWING,:HEALTHWING,:MUSCLEWING,:PRETTYWING, + :RESISTWING,:SWIFTWING + ], + 10 => [:AIRBALLOON,:BIGROOT,:BRIGHTPOWDER,:CHOICEBAND,:CHOICESCARF, + :CHOICESPECS,:DESTINYKNOT,:DISCOUNTCOUPON,:EXPERTBELT,:FOCUSBAND, + :FOCUSSASH,:LAGGINGTAIL,:LEFTOVERS,:MENTALHERB,:METALPOWDER, + :MUSCLEBAND,:POWERHERB,:QUICKPOWDER,:REAPERCLOTH,:REDCARD, + :RINGTARGET,:SHEDSHELL,:SILKSCARF,:SILVERPOWDER,:SMOOTHROCK, + :SOFTSAND,:SOOTHEBELL,:WHITEHERB,:WIDELENS,:WISEGLASSES,:ZOOMLENS, + # Terrain seeds + :ELECTRICSEED,:GRASSYSEED,:MISTYSEED,:PSYCHICSEED, + # Nectar + :PINKNECTAR,:PURPLENECTAR,:REDNECTAR,:YELLOWNECTAR, + # Incenses + :FULLINCENSE,:LAXINCENSE,:LUCKINCENSE,:ODDINCENSE,:PUREINCENSE, + :ROCKINCENSE,:ROSEINCENSE,:SEAINCENSE,:WAVEINCENSE, + # Scarves + :BLUESCARF,:GREENSCARF,:PINKSCARF,:REDSCARF,:YELLOWSCARF + ] + } + end + + def pbCheckFlingSuccess(user) + @willFail = false + @willFail = true if !user.item || !user.itemActive? || user.unlosableItem?(user.item) + return if @willFail + @willFail = true if user.item.is_berry? && !user.canConsumeBerry? + return if @willFail + return if user.item.is_mega_stone? + flingableItem = false + @flingPowers.each do |_power, items| + next if !items.include?(user.item_id) + flingableItem = true + break + end + @willFail = true if !flingableItem + end + + def pbMoveFailed?(user,targets) + if @willFail + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbDisplayUseMessage(user) + super + pbCheckFlingSuccess(user) + if !@willFail + @battle.pbDisplay(_INTL("{1} flung its {2}!",user.pbThis,user.itemName)) + end + end + + def pbNumHits(user,targets); return 1; end + + def pbBaseDamage(baseDmg,user,target) + return 10 if user.item && user.item.is_berry? + return 80 if user.item && user.item.is_mega_stone? + @flingPowers.each do |power,items| + return power if items.include?(user.item_id) + end + return 10 + end + + def pbEffectAgainstTarget(user,target) + return if target.damageState.substitute + return if target.hasActiveAbility?(:SHIELDDUST) && !@battle.moldBreaker + case user.item_id + when :POISONBARB + target.pbPoison(user) if target.pbCanPoison?(user,false,self) + when :TOXICORB + target.pbPoison(user,nil,true) if target.pbCanPoison?(user,false,self) + when :FLAMEORB + target.pbBurn(user) if target.pbCanBurn?(user,false,self) + when :LIGHTBALL + target.pbParalyze(user) if target.pbCanParalyze?(user,false,self) + when :KINGSROCK, :RAZORFANG + target.pbFlinch(user) + else + target.pbHeldItemTriggerCheck(user.item,true) + end + end + + def pbEndOfMoveUsageEffect(user,targets,numHits,switchedBattlers) + # NOTE: The item is consumed even if this move was Protected against or it + # missed. The item is not consumed if the target was switched out by + # an effect like a target's Red Card. + # NOTE: There is no item consumption animation. + user.pbConsumeItem(true,true,false) if user.item + end +end + + + +#=============================================================================== +# For 5 rounds, the target cannnot use its held item, its held item has no +# effect, and no items can be used on it. (Embargo) +#=============================================================================== +class PokeBattle_Move_0F8 < PokeBattle_Move + def pbFailsAgainstTarget?(user,target) + if target.effects[PBEffects::Embargo]>0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user,target) + target.effects[PBEffects::Embargo] = 5 + @battle.pbDisplay(_INTL("{1} can't use items anymore!",target.pbThis)) + end +end + + + +#=============================================================================== +# For 5 rounds, all held items cannot be used in any way and have no effect. +# Held items can still change hands, but can't be thrown. (Magic Room) +#=============================================================================== +class PokeBattle_Move_0F9 < PokeBattle_Move + def pbEffectGeneral(user) + if @battle.field.effects[PBEffects::MagicRoom]>0 + @battle.field.effects[PBEffects::MagicRoom] = 0 + @battle.pbDisplay(_INTL("The area returned to normal!")) + else + @battle.field.effects[PBEffects::MagicRoom] = 5 + @battle.pbDisplay(_INTL("It created a bizarre area in which Pokémon's held items lose their effects!")) + end + end + + def pbShowAnimation(id,user,targets,hitNum=0,showAnimation=true) + return if @battle.field.effects[PBEffects::MagicRoom]>0 # No animation + super + end +end + + + +#=============================================================================== +# User takes recoil damage equal to 1/4 of the damage this move dealt. +#=============================================================================== +class PokeBattle_Move_0FA < PokeBattle_RecoilMove + def pbRecoilDamage(user,target) + return (target.damageState.totalHPLost/4.0).round + end +end + + + +#=============================================================================== +# User takes recoil damage equal to 1/3 of the damage this move dealt. +#=============================================================================== +class PokeBattle_Move_0FB < PokeBattle_RecoilMove + def pbRecoilDamage(user,target) + return (target.damageState.totalHPLost/3.0).round + end +end + + + +#=============================================================================== +# User takes recoil damage equal to 1/2 of the damage this move dealt. +# (Head Smash, Light of Ruin) +#=============================================================================== +class PokeBattle_Move_0FC < PokeBattle_RecoilMove + def pbRecoilDamage(user,target) + return (target.damageState.totalHPLost/2.0).round + end +end + + + +#=============================================================================== +# User takes recoil damage equal to 1/3 of the damage this move dealt. +# May paralyze the target. (Volt Tackle) +#=============================================================================== +class PokeBattle_Move_0FD < PokeBattle_RecoilMove + def pbRecoilDamage(user,target) + return (target.damageState.totalHPLost/3.0).round + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbParalyze(user) if target.pbCanParalyze?(user,false,self) + end +end + + + +#=============================================================================== +# User takes recoil damage equal to 1/3 of the damage this move dealt. +# May burn the target. (Flare Blitz) +#=============================================================================== +class PokeBattle_Move_0FE < PokeBattle_RecoilMove + def pbRecoilDamage(user,target) + return (target.damageState.totalHPLost/3.0).round + end + + def pbAdditionalEffect(user,target) + return if target.damageState.substitute + target.pbBurn(user) if target.pbCanBurn?(user,false,self) + end +end + + + +#=============================================================================== +# Starts sunny weather. (Sunny Day) +#=============================================================================== +class PokeBattle_Move_0FF < PokeBattle_WeatherMove + def initialize(battle,move) + super + @weatherType = :Sun + end +end diff --git a/Data/Scripts/011_Battle/002_Move/007_Move_Effects_100-17F.rb b/Data/Scripts/011_Battle/002_Move/007_Move_Effects_100-17F.rb new file mode 100644 index 000000000..712505f73 --- /dev/null +++ b/Data/Scripts/011_Battle/002_Move/007_Move_Effects_100-17F.rb @@ -0,0 +1,2493 @@ +#=============================================================================== +# Starts rainy weather. (Rain Dance) +#=============================================================================== +class PokeBattle_Move_100 < PokeBattle_WeatherMove + def initialize(battle, move) + super + @weatherType = :Rain + end +end + +#=============================================================================== +# Starts sandstorm weather. (Sandstorm) +#=============================================================================== +class PokeBattle_Move_101 < PokeBattle_WeatherMove + def initialize(battle, move) + super + @weatherType = :Sandstorm + end +end + +#=============================================================================== +# Starts hail weather. (Hail) +#=============================================================================== +class PokeBattle_Move_102 < PokeBattle_WeatherMove + def initialize(battle, move) + super + @weatherType = :Hail + end +end + +#=============================================================================== +# Entry hazard. Lays spikes on the opposing side (max. 3 layers). (Spikes) +#=============================================================================== +class PokeBattle_Move_103 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOpposingSide.effects[PBEffects::Spikes] >= 3 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOpposingSide.effects[PBEffects::Spikes] += 1 + @battle.pbDisplay(_INTL("Spikes were scattered all around {1}'s feet!", + user.pbOpposingTeam(true))) + end +end + +#=============================================================================== +# Entry hazard. Lays poison spikes on the opposing side (max. 2 layers). +# (Toxic Spikes) +#=============================================================================== +class PokeBattle_Move_104 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOpposingSide.effects[PBEffects::ToxicSpikes] >= 2 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOpposingSide.effects[PBEffects::ToxicSpikes] += 1 + @battle.pbDisplay(_INTL("Poison spikes were scattered all around {1}'s feet!", + user.pbOpposingTeam(true))) + end +end + +#=============================================================================== +# Entry hazard. Lays stealth rocks on the opposing side. (Stealth Rock) +#=============================================================================== +class PokeBattle_Move_105 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOpposingSide.effects[PBEffects::StealthRock] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOpposingSide.effects[PBEffects::StealthRock] = true + @battle.pbDisplay(_INTL("Pointed stones float in the air around {1}!", + user.pbOpposingTeam(true))) + end +end + +#=============================================================================== +# Combos with another Pledge move used by the ally. (Grass Pledge) +# If the move is a combo, power is doubled and causes either a sea of fire or a +# swamp on the opposing side. +#=============================================================================== +class PokeBattle_Move_106 < PokeBattle_PledgeMove + def initialize(battle, move) + super + # [Function code to combo with, effect, override type, override animation] + @combos = [["107", :SeaOfFire, :FIRE, :FIREPLEDGE], + ["108", :Swamp, nil, nil]] + end +end + +#=============================================================================== +# Combos with another Pledge move used by the ally. (Fire Pledge) +# If the move is a combo, power is doubled and causes either a rainbow on the +# user's side or a sea of fire on the opposing side. +#=============================================================================== +class PokeBattle_Move_107 < PokeBattle_PledgeMove + def initialize(battle, move) + super + # [Function code to combo with, effect, override type, override animation] + @combos = [["108", :Rainbow, :WATER, :WATERPLEDGE], + ["106", :SeaOfFire, nil, nil]] + end +end + +#=============================================================================== +# Combos with another Pledge move used by the ally. (Water Pledge) +# If the move is a combo, power is doubled and causes either a swamp on the +# opposing side or a rainbow on the user's side. +#=============================================================================== +class PokeBattle_Move_108 < PokeBattle_PledgeMove + def initialize(battle, move) + super + # [Function code to combo with, effect, override type, override animation] + @combos = [["106", :Swamp, :GRASS, :GRASSPLEDGE], + ["107", :Rainbow, nil, nil]] + end +end + +#=============================================================================== +# Scatters coins that the player picks up after winning the battle. (Pay Day) +# NOTE: In Gen 6+, if the user levels up after this move is used, the amount of +# money picked up depends on the user's new level rather than its level +# when it used the move. I think this is sill +# y, so I haven't coded this +# effect. +#=============================================================================== +class PokeBattle_Move_109 < PokeBattle_Move + def pbEffectGeneral(user) + if user.pbOwnedByPlayer? + @battle.field.effects[PBEffects::PayDay] += 5 * user.level + end + @battle.pbDisplay(_INTL("Coins were scattered everywhere!")) + end +end + +#=============================================================================== +# Ends the opposing side's Light Screen, Reflect and Aurora Break. (Brick Break, +# Psychic Fangs) +#=============================================================================== +class PokeBattle_Move_10A < PokeBattle_Move + def ignoresReflect? + return true; + end + + def pbEffectGeneral(user) + if user.pbOpposingSide.effects[PBEffects::LightScreen] > 0 + user.pbOpposingSide.effects[PBEffects::LightScreen] = 0 + @battle.pbDisplay(_INTL("{1}'s Light Screen wore off!", user.pbOpposingTeam)) + end + if user.pbOpposingSide.effects[PBEffects::Reflect] > 0 + user.pbOpposingSide.effects[PBEffects::Reflect] = 0 + @battle.pbDisplay(_INTL("{1}'s Reflect wore off!", user.pbOpposingTeam)) + end + if user.pbOpposingSide.effects[PBEffects::AuroraVeil] > 0 + user.pbOpposingSide.effects[PBEffects::AuroraVeil] = 0 + @battle.pbDisplay(_INTL("{1}'s Aurora Veil wore off!", user.pbOpposingTeam)) + end + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + if user.pbOpposingSide.effects[PBEffects::LightScreen] > 0 || + user.pbOpposingSide.effects[PBEffects::Reflect] > 0 || + user.pbOpposingSide.effects[PBEffects::AuroraVeil] > 0 + hitNum = 1 # Wall-breaking anim + end + super + end +end + +#=============================================================================== +# If attack misses, user takes crash damage of 1/2 of max HP. +# (High Jump Kick, Jump Kick) +#=============================================================================== +class PokeBattle_Move_10B < PokeBattle_Move + def recoilMove? + return true; + end + + def unusableInGravity? + return true; + end + + def pbCrashDamage(user) + return if !user.takesIndirectDamage? + @battle.pbDisplay(_INTL("{1} kept going and crashed!", user.pbThis)) + @battle.scene.pbDamageAnimation(user) + user.pbReduceHP(user.totalhp / 2, false) + user.pbItemHPHealCheck + user.pbFaint if user.fainted? + end +end + +#=============================================================================== +# User turns 1/4 of max HP into a substitute. (Substitute) +#=============================================================================== +class PokeBattle_Move_10C < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Substitute] > 0 + @battle.pbDisplay(_INTL("{1} already has a substitute!", user.pbThis)) + return true + end + @subLife = user.totalhp / 4 + @subLife = 1 if @subLife < 1 + if user.hp <= @subLife + @battle.pbDisplay(_INTL("But it does not have enough HP left to make a substitute!")) + return true + end + return false + end + + def pbOnStartUse(user, targets) + user.pbReduceHP(@subLife, false, false) + user.pbItemHPHealCheck + end + + def pbEffectGeneral(user) + user.effects[PBEffects::Trapping] = 0 + user.effects[PBEffects::TrappingMove] = nil + user.effects[PBEffects::Substitute] = @subLife + @battle.pbDisplay(_INTL("{1} put in a substitute!", user.pbThis)) + end +end + +#=============================================================================== +# User is Ghost: User loses 1/2 of max HP, and curses the target. +# Cursed Pokémon lose 1/4 of their max HP at the end of each round. +# User is not Ghost: Decreases the user's Speed by 1 stage, and increases the +# user's Attack and Defense by 1 stage each. (Curse) +#=============================================================================== +class PokeBattle_Move_10D < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbTarget(user) + return GameData::Target.get(:NearFoe) if user.pbHasType?(:GHOST) + return super + end + + def pbMoveFailed?(user, targets) + return false if user.pbHasType?(:GHOST) + if !user.pbCanLowerStatStage?(:SPEED, user, self) && + !user.pbCanRaiseStatStage?(:ATTACK, user, self) && + !user.pbCanRaiseStatStage?(:DEFENSE, user, self) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + if user.pbHasType?(:GHOST) && target.effects[PBEffects::Curse] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + return if user.pbHasType?(:GHOST) + # Non-Ghost effect + if user.pbCanLowerStatStage?(:SPEED, user, self) + user.pbLowerStatStage(:SPEED, 1, user) + end + showAnim = true + if user.pbCanRaiseStatStage?(:ATTACK, user, self) + if user.pbRaiseStatStage(:ATTACK, 1, user, showAnim) + showAnim = false + end + end + if user.pbCanRaiseStatStage?(:DEFENSE, user, self) + user.pbRaiseStatStage(:DEFENSE, 1, user, showAnim) + end + end + + def pbEffectAgainstTarget(user, target) + return if !user.pbHasType?(:GHOST) + # Ghost effect + @battle.pbDisplay(_INTL("{1} cut its own HP and laid a curse on {2}!", user.pbThis, target.pbThis(true))) + target.effects[PBEffects::Curse] = true + user.pbReduceHP(user.totalhp / 2, false) + user.pbItemHPHealCheck + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + hitNum = 1 if !user.pbHasType?(:GHOST) # Non-Ghost anim + super + end +end + +#=============================================================================== +# Target's last move used loses 4 PP. (Spite) +#=============================================================================== +class PokeBattle_Move_10E < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbFailsAgainstTarget?(user, target) + failed = true + if target.lastRegularMoveUsed + target.eachMove do |m| + next if m.id != target.lastRegularMoveUsed || m.pp == 0 || m.total_pp <= 0 + failed = false; break + end + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.eachMove do |m| + next if m.id != target.lastRegularMoveUsed + reduction = [4, m.pp].min + target.pbSetPP(m, m.pp - reduction) + @battle.pbDisplay(_INTL("It reduced the PP of {1}'s {2} by {3}!", + target.pbThis(true), m.name, reduction)) + break + end + end +end + +#=============================================================================== +# Target will lose 1/4 of max HP at end of each round, while asleep. (Nightmare) +#=============================================================================== +class PokeBattle_Move_10F < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if !target.asleep? || target.effects[PBEffects::Nightmare] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Nightmare] = true + @battle.pbDisplay(_INTL("{1} began having a nightmare!", target.pbThis)) + end +end + +#=============================================================================== +# Removes trapping moves, entry hazards and Leech Seed on user/user's side. +# (Rapid Spin) +#=============================================================================== +class PokeBattle_Move_110 < PokeBattle_Move + def pbEffectAfterAllHits(user, target) + return if user.fainted? || target.damageState.unaffected + if user.effects[PBEffects::Trapping] > 0 + trapMove = GameData::Move.get(user.effects[PBEffects::TrappingMove]).name + trapUser = @battle.battlers[user.effects[PBEffects::TrappingUser]] + @battle.pbDisplay(_INTL("{1} got free of {2}'s {3}!", user.pbThis, trapUser.pbThis(true), trapMove)) + user.effects[PBEffects::Trapping] = 0 + user.effects[PBEffects::TrappingMove] = nil + user.effects[PBEffects::TrappingUser] = -1 + end + if user.effects[PBEffects::LeechSeed] >= 0 + user.effects[PBEffects::LeechSeed] = -1 + @battle.pbDisplay(_INTL("{1} shed Leech Seed!", user.pbThis)) + end + if user.pbOwnSide.effects[PBEffects::StealthRock] + user.pbOwnSide.effects[PBEffects::StealthRock] = false + @battle.pbDisplay(_INTL("{1} blew away stealth rocks!", user.pbThis)) + end + if user.pbOwnSide.effects[PBEffects::Spikes] > 0 + user.pbOwnSide.effects[PBEffects::Spikes] = 0 + @battle.pbDisplay(_INTL("{1} blew away spikes!", user.pbThis)) + end + if user.pbOwnSide.effects[PBEffects::ToxicSpikes] > 0 + user.pbOwnSide.effects[PBEffects::ToxicSpikes] = 0 + @battle.pbDisplay(_INTL("{1} blew away poison spikes!", user.pbThis)) + end + if user.pbOwnSide.effects[PBEffects::StickyWeb] + user.pbOwnSide.effects[PBEffects::StickyWeb] = false + @battle.pbDisplay(_INTL("{1} blew away sticky webs!", user.pbThis)) + end + end +end + +#=============================================================================== +# Attacks 2 rounds in the future. (Doom Desire, Future Sight) +#=============================================================================== +class PokeBattle_Move_111 < PokeBattle_Move + def cannotRedirect? + return true; + end + + def pbDamagingMove? # Stops damage being dealt in the setting-up turn + return false if !@battle.futureSight + return super + end + + def pbAccuracyCheck(user, target) + return true if !@battle.futureSight + return super + end + + def pbDisplayUseMessage(user) + super if !@battle.futureSight + end + + def pbFailsAgainstTarget?(user, target) + if !@battle.futureSight && + @battle.positions[target.index].effects[PBEffects::FutureSightCounter] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + return if @battle.futureSight # Attack is hitting + effects = @battle.positions[target.index].effects + effects[PBEffects::FutureSightCounter] = 3 + effects[PBEffects::FutureSightMove] = @id + effects[PBEffects::FutureSightUserIndex] = user.index + effects[PBEffects::FutureSightUserPartyIndex] = user.pokemonIndex + if @id == :DOOMDESIRE + @battle.pbDisplay(_INTL("{1} chose Doom Desire as its destiny!", user.pbThis)) + else + @battle.pbDisplay(_INTL("{1} foresaw an attack!", user.pbThis)) + end + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + hitNum = 1 if !@battle.futureSight # Charging anim + super + end +end + +#=============================================================================== +# Increases the user's Defense and Special Defense by 1 stage each. Ups the +# user's stockpile by 1 (max. 3). (Stockpile) +#=============================================================================== +class PokeBattle_Move_112 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Stockpile] >= 3 + @battle.pbDisplay(_INTL("{1} can't stockpile any more!", user.pbThis)) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::Stockpile] += 1 + @battle.pbDisplay(_INTL("{1} stockpiled {2}!", + user.pbThis, user.effects[PBEffects::Stockpile])) + showAnim = true + if user.pbCanRaiseStatStage?(:DEFENSE, user, self) + if user.pbRaiseStatStage(:DEFENSE, 1, user, showAnim) + user.effects[PBEffects::StockpileDef] += 1 + showAnim = false + end + end + if user.pbCanRaiseStatStage?(:SPECIAL_DEFENSE, user, self) + if user.pbRaiseStatStage(:SPECIAL_DEFENSE, 1, user, showAnim) + user.effects[PBEffects::StockpileSpDef] += 1 + end + end + end +end + +#=============================================================================== +# Power is 100 multiplied by the user's stockpile (X). Resets the stockpile to +# 0. Decreases the user's Defense and Special Defense by X stages each. (Spit Up) +#=============================================================================== +class PokeBattle_Move_113 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Stockpile] == 0 + @battle.pbDisplay(_INTL("But it failed to spit up a thing!")) + return true + end + return false + end + + def pbBaseDamage(baseDmg, user, target) + return 100 * user.effects[PBEffects::Stockpile] + end + + def pbEffectAfterAllHits(user, target) + return if user.fainted? || user.effects[PBEffects::Stockpile] == 0 + return if target.damageState.unaffected + @battle.pbDisplay(_INTL("{1}'s stockpiled effect wore off!", user.pbThis)) + return if @battle.pbAllFainted?(target.idxOwnSide) + showAnim = true + if user.effects[PBEffects::StockpileDef] > 0 && + user.pbCanLowerStatStage?(:DEFENSE, user, self) + if user.pbLowerStatStage(:DEFENSE, user.effects[PBEffects::StockpileDef], user, showAnim) + showAnim = false + end + end + if user.effects[PBEffects::StockpileSpDef] > 0 && + user.pbCanLowerStatStage?(:SPECIAL_DEFENSE, user, self) + user.pbLowerStatStage(:SPECIAL_DEFENSE, user.effects[PBEffects::StockpileSpDef], user, showAnim) + end + user.effects[PBEffects::Stockpile] = 0 + user.effects[PBEffects::StockpileDef] = 0 + user.effects[PBEffects::StockpileSpDef] = 0 + end +end + +#=============================================================================== +# Heals user depending on the user's stockpile (X). Resets the stockpile to 0. +# Decreases the user's Defense and Special Defense by X stages each. (Swallow) +#=============================================================================== +class PokeBattle_Move_114 < PokeBattle_Move + def healingMove? + return true; + end + + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Stockpile] == 0 + @battle.pbDisplay(_INTL("But it failed to swallow a thing!")) + return true + end + if !user.canHeal? && + user.effects[PBEffects::StockpileDef] == 0 && + user.effects[PBEffects::StockpileSpDef] == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + hpGain = 0 + case [user.effects[PBEffects::Stockpile], 1].max + when 1 then + hpGain = user.totalhp / 4 + when 2 then + hpGain = user.totalhp / 2 + when 3 then + hpGain = user.totalhp + end + if user.pbRecoverHP(hpGain) > 0 + @battle.pbDisplay(_INTL("{1}'s HP was restored.", user.pbThis)) + end + @battle.pbDisplay(_INTL("{1}'s stockpiled effect wore off!", user.pbThis)) + showAnim = true + if user.effects[PBEffects::StockpileDef] > 0 && + user.pbCanLowerStatStage?(:DEFENSE, user, self) + if user.pbLowerStatStage(:DEFENSE, user.effects[PBEffects::StockpileDef], user, showAnim) + showAnim = false + end + end + if user.effects[PBEffects::StockpileSpDef] > 0 && + user.pbCanLowerStatStage?(:SPECIAL_DEFENSE, user, self) + user.pbLowerStatStage(:SPECIAL_DEFENSE, user.effects[PBEffects::StockpileSpDef], user, showAnim) + end + user.effects[PBEffects::Stockpile] = 0 + user.effects[PBEffects::StockpileDef] = 0 + user.effects[PBEffects::StockpileSpDef] = 0 + end +end + +#=============================================================================== +# Fails if user was hit by a damaging move this round. (Focus Punch) +#=============================================================================== +class PokeBattle_Move_115 < PokeBattle_Move + def pbDisplayChargeMessage(user) + user.effects[PBEffects::FocusPunch] = true + @battle.pbCommonAnimation("FocusPunch", user) + @battle.pbDisplay(_INTL("{1} is tightening its focus!", user.pbThis)) + end + + def pbDisplayUseMessage(user) + super if !user.effects[PBEffects::FocusPunch] || user.lastHPLost == 0 + end + + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::FocusPunch] && user.lastHPLost > 0 + @battle.pbDisplay(_INTL("{1} lost its focus and couldn't move!", user.pbThis)) + return true + end + return false + end +end + +#=============================================================================== +# Fails if the target didn't chose a damaging move to use this round, or has +# already moved. (Sucker Punch) +#=============================================================================== +class PokeBattle_Move_116 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if @battle.choices[target.index][0] != :UseMove + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + oppMove = @battle.choices[target.index][2] + if !oppMove || + (oppMove.function != "0B0" && # Me First + (target.movedThisRound? || oppMove.statusMove?)) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# This round, user becomes the target of attacks that have single targets. +# (Follow Me, Rage Powder) +#=============================================================================== +class PokeBattle_Move_117 < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::FollowMe] = 1 + user.eachAlly do |b| + next if b.effects[PBEffects::FollowMe] < user.effects[PBEffects::FollowMe] + user.effects[PBEffects::FollowMe] = b.effects[PBEffects::FollowMe] + 1 + end + user.effects[PBEffects::RagePowder] = true if @id == :RAGEPOWDER + @battle.pbDisplay(_INTL("{1} became the center of attention!", user.pbThis)) + end +end + +#=============================================================================== +# For 5 rounds, increases gravity on the field. Pokémon cannot become airborne. +# (Gravity) +#=============================================================================== +class PokeBattle_Move_118 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.field.effects[PBEffects::Gravity] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.field.effects[PBEffects::Gravity] = 5 + @battle.pbDisplay(_INTL("Gravity intensified!")) + @battle.eachBattler do |b| + showMessage = false + if b.inTwoTurnAttack?("0C9", "0CC", "0CE") # Fly/Bounce/Sky Drop + b.effects[PBEffects::TwoTurnAttack] = nil + @battle.pbClearChoice(b.index) if !b.movedThisRound? + showMessage = true + end + if b.effects[PBEffects::MagnetRise] > 0 || + b.effects[PBEffects::Telekinesis] > 0 || + b.effects[PBEffects::SkyDrop] >= 0 + b.effects[PBEffects::MagnetRise] = 0 + b.effects[PBEffects::Telekinesis] = 0 + b.effects[PBEffects::SkyDrop] = -1 + showMessage = true + end + @battle.pbDisplay(_INTL("{1} couldn't stay airborne because of gravity!", + b.pbThis)) if showMessage + end + end +end + +#=============================================================================== +# For 5 rounds, user becomes airborne. (Magnet Rise) +#=============================================================================== +class PokeBattle_Move_119 < PokeBattle_Move + def unusableInGravity? + return true; + end + + def pbMoveFailed?(user, targets) + if user.effects[PBEffects::Ingrain] || + user.effects[PBEffects::SmackDown] || + user.effects[PBEffects::MagnetRise] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.effects[PBEffects::MagnetRise] = 5 + @battle.pbDisplay(_INTL("{1} levitated with electromagnetism!", user.pbThis)) + end +end + +#=============================================================================== +# For 3 rounds, target becomes airborne and can always be hit. (Telekinesis) +#=============================================================================== +class PokeBattle_Move_11A < PokeBattle_Move + def unusableInGravity? + return true; + end + + def pbFailsAgainstTarget?(user, target) + if target.effects[PBEffects::Ingrain] || + target.effects[PBEffects::SmackDown] || + target.effects[PBEffects::Telekinesis] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.isSpecies?(:DIGLETT) || + target.isSpecies?(:DUGTRIO) || + target.isSpecies?(:SANDYGAST) || + target.isSpecies?(:PALOSSAND) || + (target.isSpecies?(:GENGAR) && target.mega?) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Telekinesis] = 3 + @battle.pbDisplay(_INTL("{1} was hurled into the air!", target.pbThis)) + end +end + +#=============================================================================== +# Hits airborne semi-invulnerable targets. (Sky Uppercut) +#=============================================================================== +class PokeBattle_Move_11B < PokeBattle_Move + def hitsFlyingTargets? + return true; + end +end + +#=============================================================================== +# Grounds the target while it remains active. Hits some semi-invulnerable +# targets. (Smack Down, Thousand Arrows) +#=============================================================================== +class PokeBattle_Move_11C < PokeBattle_Move + def hitsFlyingTargets? + return true; + end + + def pbCalcTypeModSingle(moveType, defType, user, target) + return Effectiveness::NORMAL_EFFECTIVE_ONE if moveType == :GROUND && defType == :FLYING + return super + end + + def pbEffectAfterAllHits(user, target) + return if target.fainted? + return if target.damageState.unaffected || target.damageState.substitute + return if target.inTwoTurnAttack?("0CE") || target.effects[PBEffects::SkyDrop] >= 0 # Sky Drop + return if !target.airborne? && !target.inTwoTurnAttack?("0C9", "0CC") # Fly/Bounce + target.effects[PBEffects::SmackDown] = true + if target.inTwoTurnAttack?("0C9", "0CC") # Fly/Bounce. NOTE: Not Sky Drop. + target.effects[PBEffects::TwoTurnAttack] = nil + @battle.pbClearChoice(target.index) if !target.movedThisRound? + end + target.effects[PBEffects::MagnetRise] = 0 + target.effects[PBEffects::Telekinesis] = 0 + @battle.pbDisplay(_INTL("{1} fell straight down!", target.pbThis)) + end +end + +#=============================================================================== +# Target moves immediately after the user, ignoring priority/speed. (After You) +#=============================================================================== +class PokeBattle_Move_11D < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbFailsAgainstTarget?(user, target) + # Target has already moved this round + return true if pbMoveFailedTargetAlreadyMoved?(target) + # Target was going to move next anyway (somehow) + if target.effects[PBEffects::MoveNext] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + # Target didn't choose to use a move this round + oppMove = @battle.choices[target.index][2] + if !oppMove + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::MoveNext] = true + target.effects[PBEffects::Quash] = 0 + @battle.pbDisplay(_INTL("{1} took the kind offer!", target.pbThis)) + end +end + +#=============================================================================== +# Target moves last this round, ignoring priority/speed. (Quash) +#=============================================================================== +class PokeBattle_Move_11E < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + return true if pbMoveFailedTargetAlreadyMoved?(target) + # Target isn't going to use a move + oppMove = @battle.choices[target.index][2] + if !oppMove + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + # Target is already maximally Quashed and will move last anyway + highestQuash = 0 + @battle.battlers.each do |b| + next if b.effects[PBEffects::Quash] <= highestQuash + highestQuash = b.effects[PBEffects::Quash] + end + if highestQuash > 0 && target.effects[PBEffects::Quash] == highestQuash + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + # Target was already going to move last + if highestQuash == 0 && @battle.pbPriority.last.index == target.index + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + highestQuash = 0 + @battle.battlers.each do |b| + next if b.effects[PBEffects::Quash] <= highestQuash + highestQuash = b.effects[PBEffects::Quash] + end + target.effects[PBEffects::Quash] = highestQuash + 1 + target.effects[PBEffects::MoveNext] = false + @battle.pbDisplay(_INTL("{1}'s move was postponed!", target.pbThis)) + end +end + +#=============================================================================== +# For 5 rounds, for each priority bracket, slow Pokémon move before fast ones. +# (Trick Room) +#=============================================================================== +class PokeBattle_Move_11F < PokeBattle_Move + def pbEffectGeneral(user) + if @battle.field.effects[PBEffects::TrickRoom] > 0 + @battle.field.effects[PBEffects::TrickRoom] = 0 + @battle.pbDisplay(_INTL("{1} reverted the dimensions!", user.pbThis)) + else + @battle.field.effects[PBEffects::TrickRoom] = 5 + @battle.pbDisplay(_INTL("{1} twisted the dimensions!", user.pbThis)) + end + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + return if @battle.field.effects[PBEffects::TrickRoom] > 0 # No animation + super + end +end + +#=============================================================================== +# User switches places with its ally. (Ally Switch) +#=============================================================================== +class PokeBattle_Move_120 < PokeBattle_Move + def pbMoveFailed?(user, targets) + numTargets = 0 + @idxAlly = -1 + idxUserOwner = @battle.pbGetOwnerIndexFromBattlerIndex(user.index) + user.eachAlly do |b| + next if @battle.pbGetOwnerIndexFromBattlerIndex(b.index) != idxUserOwner + next if !b.near?(user) + numTargets += 1 + @idxAlly = b.index + end + if numTargets != 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + idxA = user.index + idxB = @idxAlly + if @battle.pbSwapBattlers(idxA, idxB) + @battle.pbDisplay(_INTL("{1} and {2} switched places!", + @battle.battlers[idxB].pbThis, @battle.battlers[idxA].pbThis(true))) + end + end +end + +#=============================================================================== +# Target's Attack is used instead of user's Attack for this move's calculations. +# (Foul Play) +#=============================================================================== +class PokeBattle_Move_121 < PokeBattle_Move + def pbGetAttackStats(user, target) + if specialMove? + return target.spatk, target.stages[:SPECIAL_ATTACK] + 6 + end + return target.attack, target.stages[:ATTACK] + 6 + end +end + +#=============================================================================== +# Target's Defense is used instead of its Special Defense for this move's +# calculations. (Psyshock, Psystrike, Secret Sword) +#=============================================================================== +class PokeBattle_Move_122 < PokeBattle_Move + def pbGetDefenseStats(user, target) + return target.defense, target.stages[:DEFENSE] + 6 + end +end + +#=============================================================================== +# Only damages Pokémon that share a type with the user. (Synchronoise) +#=============================================================================== +class PokeBattle_Move_123 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + userTypes = user.pbTypes(true) + targetTypes = target.pbTypes(true) + sharesType = false + userTypes.each do |t| + next if !targetTypes.include?(t) + sharesType = true + break + end + if !sharesType + @battle.pbDisplay(_INTL("{1} is unaffected!", target.pbThis)) + return true + end + return false + end +end + +#=============================================================================== +# For 5 rounds, swaps all battlers' base Defense with base Special Defense. +# (Wonder Room) +#=============================================================================== +class PokeBattle_Move_124 < PokeBattle_Move + def pbEffectGeneral(user) + if @battle.field.effects[PBEffects::WonderRoom] > 0 + @battle.field.effects[PBEffects::WonderRoom] = 0 + @battle.pbDisplay(_INTL("Wonder Room wore off, and the Defense and Sp. Def stats returned to normal!")) + else + @battle.field.effects[PBEffects::WonderRoom] = 5 + @battle.pbDisplay(_INTL("It created a bizarre area in which the Defense and Sp. Def stats are swapped!")) + end + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + return if @battle.field.effects[PBEffects::WonderRoom] > 0 # No animation + super + end +end + +#=============================================================================== +# Fails unless user has already used all other moves it knows. (Last Resort) +#=============================================================================== +class PokeBattle_Move_125 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + hasThisMove = false; hasOtherMoves = false; hasUnusedMoves = false + user.eachMove do |m| + hasThisMove = true if m.id == @id + hasOtherMoves = true if m.id != @id + hasUnusedMoves = true if m.id != @id && !user.movesUsed.include?(m.id) + end + if !hasThisMove || !hasOtherMoves || hasUnusedMoves + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# NOTE: Shadow moves use function codes 126-132 inclusive. +#=============================================================================== + +#=============================================================================== +# Does absolutely nothing. (Hold Hands) +#=============================================================================== +class PokeBattle_Move_133 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + hasAlly = false + user.eachAlly do |_b| + hasAlly = true + break + end + if !hasAlly + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# Does absolutely nothing. Shows a special message. (Celebrate) +#=============================================================================== +class PokeBattle_Move_134 < PokeBattle_Move + def pbEffectGeneral(user) + if @battle.wildBattle? && user.opposes? + @battle.pbDisplay(_INTL("Congratulations from {1}!", user.pbThis(true))) + else + @battle.pbDisplay(_INTL("Congratulations, {1}!", @battle.pbGetOwnerName(user.index))) + end + end +end + +#=============================================================================== +# Freezes the target. Effectiveness against Water-type is 2x. (Freeze-Dry) +#=============================================================================== +class PokeBattle_Move_135 < PokeBattle_FreezeMove + def pbCalcTypeModSingle(moveType, defType, user, target) + return Effectiveness::SUPER_EFFECTIVE_ONE if defType == :WATER + return super + end +end + +#=============================================================================== +# Increases the user's Defense by 2 stages. (Diamond Storm) +#=============================================================================== +class PokeBattle_Move_136 < PokeBattle_Move_02F + # NOTE: In Gen 6, this move increased the user's Defense by 1 stage for each + # target it hit. This effect changed in Gen 7 and is now identical to + # function code 02F. +end + +#=============================================================================== +# Increases the user's and its ally's Defense and Special Defense by 1 stage +# each, if they have Plus or Minus. (Magnetic Flux) +#=============================================================================== +# NOTE: In Gen 5, this move should have a target of UserSide, while in Gen 6+ it +# should have a target of UserAndAllies. This is because, in Gen 5, this +# move shouldn't call def pbSuccessCheckAgainstTarget for each Pokémon +# currently in battle that will be affected by this move (i.e. allies +# aren't protected by their substitute/ability/etc., but they are in Gen +# 6+). We achieve this by not targeting any battlers in Gen 5, since +# pbSuccessCheckAgainstTarget is only called for targeted battlers. +class PokeBattle_Move_137 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + @validTargets = [] + @battle.eachSameSideBattler(user) do |b| + next if !b.hasActiveAbility?([:MINUS, :PLUS]) + next if !b.pbCanRaiseStatStage?(:DEFENSE, user, self) && + !b.pbCanRaiseStatStage?(:SPECIAL_DEFENSE, user, self) + @validTargets.push(b) + end + if @validTargets.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + return false if @validTargets.any? { |b| b.index == target.index } + return true if !target.hasActiveAbility?([:MINUS, :PLUS]) + @battle.pbDisplay(_INTL("{1}'s stats can't be raised further!", target.pbThis)) + return true + end + + def pbEffectAgainstTarget(user, target) + showAnim = true + if target.pbCanRaiseStatStage?(:DEFENSE, user, self) + if target.pbRaiseStatStage(:DEFENSE, 1, user, showAnim) + showAnim = false + end + end + if target.pbCanRaiseStatStage?(:SPECIAL_DEFENSE, user, self) + target.pbRaiseStatStage(:SPECIAL_DEFENSE, 1, user, showAnim) + end + end + + def pbEffectGeneral(user) + return if pbTarget(user) != :UserSide + @validTargets.each { |b| pbEffectAgainstTarget(user, b) } + end +end + +#=============================================================================== +# Increases target's Special Defense by 1 stage. (Aromatic Mist) +#=============================================================================== +class PokeBattle_Move_138 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbFailsAgainstTarget?(user, target) + return true if !target.pbCanRaiseStatStage?(:SPECIAL_DEFENSE, user, self, true) + return false + end + + def pbEffectAgainstTarget(user, target) + target.pbRaiseStatStage(:SPECIAL_DEFENSE, 1, user) + end +end + +#=============================================================================== +# Decreases the target's Attack by 1 stage. Always hits. (Play Nice) +#=============================================================================== +class PokeBattle_Move_139 < PokeBattle_TargetStatDownMove + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @statDown = [:ATTACK, 1] + end + + def pbAccuracyCheck(user, target) + ; return true; + end +end + +#=============================================================================== +# Decreases the target's Attack and Special Attack by 1 stage each. Always hits. +# (Noble Roar) +#=============================================================================== +class PokeBattle_Move_13A < PokeBattle_TargetMultiStatDownMove + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @statDown = [:ATTACK, 1, :SPECIAL_ATTACK, 1] + end + + def pbAccuracyCheck(user, target) + ; return true; + end +end + +#=============================================================================== +# Decreases the user's Defense by 1 stage. Always hits. Ends target's +# protections immediately. (Hyperspace Fury) +#=============================================================================== +class PokeBattle_Move_13B < PokeBattle_StatDownMove + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @statDown = [:DEFENSE, 1] + end + + def pbMoveFailed?(user, targets) + if !user.isSpecies?(:HOOPA) + @battle.pbDisplay(_INTL("But {1} can't use the move!", user.pbThis(true))) + return true + elsif user.form != 1 + @battle.pbDisplay(_INTL("But {1} can't use it the way it is now!", user.pbThis(true))) + return true + end + return false + end + + def pbAccuracyCheck(user, target) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::BanefulBunker] = false + target.effects[PBEffects::KingsShield] = false + target.effects[PBEffects::Protect] = false + target.effects[PBEffects::SpikyShield] = false + target.pbOwnSide.effects[PBEffects::CraftyShield] = false + target.pbOwnSide.effects[PBEffects::MatBlock] = false + target.pbOwnSide.effects[PBEffects::QuickGuard] = false + target.pbOwnSide.effects[PBEffects::WideGuard] = false + end +end + +#=============================================================================== +# Decreases the target's Special Attack by 1 stage. Always hits. (Confide) +#=============================================================================== +class PokeBattle_Move_13C < PokeBattle_TargetStatDownMove + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @statDown = [:SPECIAL_ATTACK, 1] + end + + def pbAccuracyCheck(user, target) + ; return true; + end +end + +#=============================================================================== +# Decreases the target's Special Attack by 2 stages. (Eerie Impulse) +#=============================================================================== +class PokeBattle_Move_13D < PokeBattle_TargetStatDownMove + def initialize(battle, move) + super + @statDown = [:SPECIAL_ATTACK, 2] + end +end + +#=============================================================================== +# Increases the Attack and Special Attack of all Grass-type Pokémon in battle by +# 1 stage each. Doesn't affect airborne Pokémon. (Rototiller) +#=============================================================================== +class PokeBattle_Move_13E < PokeBattle_Move + def pbMoveFailed?(user, targets) + @validTargets = [] + @battle.eachBattler do |b| + next if !b.pbHasType?(:GRASS) + next if b.airborne? || b.semiInvulnerable? + next if !b.pbCanRaiseStatStage?(:ATTACK, user, self) && + !b.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) + @validTargets.push(b.index) + end + if @validTargets.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + return false if @validTargets.include?(target.index) + return true if !target.pbHasType?(:GRASS) + return true if target.airborne? || target.semiInvulnerable? + @battle.pbDisplay(_INTL("{1}'s stats can't be raised further!", target.pbThis)) + return true + end + + def pbEffectAgainstTarget(user, target) + showAnim = true + if target.pbCanRaiseStatStage?(:ATTACK, user, self) + if target.pbRaiseStatStage(:ATTACK, 1, user, showAnim) + showAnim = false + end + end + if target.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) + target.pbRaiseStatStage(:SPECIAL_ATTACK, 1, user, showAnim) + end + end +end + +#=============================================================================== +# Increases the Defense of all Grass-type Pokémon on the field by 1 stage each. +# (Flower Shield) +#=============================================================================== +class PokeBattle_Move_13F < PokeBattle_Move + def pbMoveFailed?(user, targets) + @validTargets = [] + @battle.eachBattler do |b| + next if !b.pbHasType?(:GRASS) + next if b.semiInvulnerable? + next if !b.pbCanRaiseStatStage?(:DEFENSE, user, self) + @validTargets.push(b.index) + end + if @validTargets.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + return false if @validTargets.include?(target.index) + return true if !target.pbHasType?(:GRASS) || target.semiInvulnerable? + return !target.pbCanRaiseStatStage?(:DEFENSE, user, self, true) + end + + def pbEffectAgainstTarget(user, target) + target.pbRaiseStatStage(:DEFENSE, 1, user) + end +end + +#=============================================================================== +# Decreases the Attack, Special Attack and Speed of all poisoned targets by 1 +# stage each. (Venom Drench) +#=============================================================================== +class PokeBattle_Move_140 < PokeBattle_Move + def pbMoveFailed?(user, targets) + @validTargets = [] + targets.each do |b| + next if !b || b.fainted? + next if !b.poisoned? + next if !b.pbCanLowerStatStage?(:ATTACK, user, self) && + !b.pbCanLowerStatStage?(:SPECIAL_ATTACK, user, self) && + !b.pbCanLowerStatStage?(:SPEED, user, self) + @validTargets.push(b.index) + end + if @validTargets.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + return if !@validTargets.include?(target.index) + showAnim = true + [:ATTACK, :SPECIAL_ATTACK, :SPEED].each do |s| + next if !target.pbCanLowerStatStage?(s, user, self) + if target.pbLowerStatStage(s, 1, user, showAnim) + showAnim = false + end + end + end +end + +#=============================================================================== +# Reverses all stat changes of the target. (Topsy-Turvy) +#=============================================================================== +class PokeBattle_Move_141 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + failed = true + GameData::Stat.each_battle do |s| + next if target.stages[s.id] == 0 + failed = false + break + end + if failed + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + GameData::Stat.each_battle { |s| target.stages[s.id] *= -1 } + @battle.pbDisplay(_INTL("{1}'s stats were reversed!", target.pbThis)) + end +end + +#=============================================================================== +# Gives target the Ghost type. (Trick-or-Treat) +#=============================================================================== +class PokeBattle_Move_142 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if !GameData::Type.exists?(:GHOST) || target.pbHasType?(:GHOST) || !target.canChangeType? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Type3] = :GHOST + typeName = GameData::Type.get(:GHOST).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", target.pbThis, typeName)) + end +end + +#=============================================================================== +# Gives target the Grass type. (Forest's Curse) +#=============================================================================== +class PokeBattle_Move_143 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if !GameData::Type.exists?(:GRASS) || target.pbHasType?(:GRASS) || !target.canChangeType? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Type3] = :GRASS + typeName = GameData::Type.get(:GRASS).name + @battle.pbDisplay(_INTL("{1} transformed into the {2} type!", target.pbThis, typeName)) + end +end + +#=============================================================================== +# Type effectiveness is multiplied by the Flying-type's effectiveness against +# the target. Does double damage and has perfect accuracy if the target is +# Minimized. (Flying Press) +#=============================================================================== +class PokeBattle_Move_144 < PokeBattle_Move + def tramplesMinimize?(param = 1) + return true if param == 1 && Settings::MECHANICS_GENERATION >= 6 # Perfect accuracy + return true if param == 2 # Double damage + return super + end + + def pbCalcTypeModSingle(moveType, defType, user, target) + ret = super + if GameData::Type.exists?(:FLYING) + flyingEff = Effectiveness.calculate_one(:FLYING, defType) + ret *= flyingEff.to_f / Effectiveness::NORMAL_EFFECTIVE_ONE + end + return ret + end +end + +#=============================================================================== +# Target's moves become Electric-type for the rest of the round. (Electrify) +#=============================================================================== +class PokeBattle_Move_145 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if target.effects[PBEffects::Electrify] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedTargetAlreadyMoved?(target) + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Electrify] = true + @battle.pbDisplay(_INTL("{1}'s moves have been electrified!", target.pbThis)) + end +end + +#=============================================================================== +# All Normal-type moves become Electric-type for the rest of the round. +# (Ion Deluge, Plasma Fists) +#=============================================================================== +class PokeBattle_Move_146 < PokeBattle_Move + def pbMoveFailed?(user, targets) + return false if damagingMove? + if @battle.field.effects[PBEffects::IonDeluge] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedLastInRound?(user) + return false + end + + def pbEffectGeneral(user) + return if @battle.field.effects[PBEffects::IonDeluge] + @battle.field.effects[PBEffects::IonDeluge] = true + @battle.pbDisplay(_INTL("A deluge of ions showers the battlefield!")) + end +end + +#=============================================================================== +# Always hits. Ends target's protections immediately. (Hyperspace Hole) +#=============================================================================== +class PokeBattle_Move_147 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbAccuracyCheck(user, target) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::BanefulBunker] = false + target.effects[PBEffects::KingsShield] = false + target.effects[PBEffects::Protect] = false + target.effects[PBEffects::SpikyShield] = false + target.pbOwnSide.effects[PBEffects::CraftyShield] = false + target.pbOwnSide.effects[PBEffects::MatBlock] = false + target.pbOwnSide.effects[PBEffects::QuickGuard] = false + target.pbOwnSide.effects[PBEffects::WideGuard] = false + end +end + +#=============================================================================== +# Powders the foe. This round, if it uses a Fire move, it loses 1/4 of its max +# HP instead. (Powder) +#=============================================================================== +class PokeBattle_Move_148 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbFailsAgainstTarget?(user, target) + if target.effects[PBEffects::Powder] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Powder] = true + @battle.pbDisplay(_INTL("{1} is covered in powder!", user.pbThis)) + end +end + +#=============================================================================== +# This round, the user's side is unaffected by damaging moves. (Mat Block) +#=============================================================================== +class PokeBattle_Move_149 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.turnCount > 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedLastInRound?(user) + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::MatBlock] = true + @battle.pbDisplay(_INTL("{1} intends to flip up a mat and block incoming attacks!", user.pbThis)) + end +end + +#=============================================================================== +# User's side is protected against status moves this round. (Crafty Shield) +#=============================================================================== +class PokeBattle_Move_14A < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOwnSide.effects[PBEffects::CraftyShield] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return true if pbMoveFailedLastInRound?(user) + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::CraftyShield] = true + @battle.pbDisplay(_INTL("Crafty Shield protected {1}!", user.pbTeam(true))) + end +end + +#=============================================================================== +# User is protected against damaging moves this round. Decreases the Attack of +# the user of a stopped contact move by 2 stages. (King's Shield) +#=============================================================================== +class PokeBattle_Move_14B < PokeBattle_ProtectMove + def initialize(battle, move) + super + @effect = PBEffects::KingsShield + end +end + +#=============================================================================== +# User is protected against moves that target it this round. Damages the user of +# a stopped contact move by 1/8 of its max HP. (Spiky Shield) +#=============================================================================== +class PokeBattle_Move_14C < PokeBattle_ProtectMove + def initialize(battle, move) + super + @effect = PBEffects::SpikyShield + end +end + +#=============================================================================== +# Two turn attack. Skips first turn, attacks second turn. (Phantom Force) +# Is invulnerable during use. Ends target's protections upon hit. +#=============================================================================== +class PokeBattle_Move_14D < PokeBattle_Move_0CD + # NOTE: This move is identical to function code 0CD (Shadow Force). +end + +#=============================================================================== +# Two turn attack. Skips first turn, and increases the user's Special Attack, +# Special Defense and Speed by 2 stages each in the second turn. (Geomancy) +#=============================================================================== +class PokeBattle_Move_14E < PokeBattle_TwoTurnMove + def pbMoveFailed?(user, targets) + return false if user.effects[PBEffects::TwoTurnAttack] # Charging turn + if !user.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) && + !user.pbCanRaiseStatStage?(:SPECIAL_DEFENSE, user, self) && + !user.pbCanRaiseStatStage?(:SPEED, user, self) + @battle.pbDisplay(_INTL("{1}'s stats won't go any higher!", user.pbThis)) + return true + end + return false + end + + def pbChargingTurnMessage(user, targets) + @battle.pbDisplay(_INTL("{1} is absorbing power!", user.pbThis)) + end + + def pbEffectGeneral(user) + return if !@damagingTurn + showAnim = true + [:SPECIAL_ATTACK, :SPECIAL_DEFENSE, :SPEED].each do |s| + next if !user.pbCanRaiseStatStage?(s, user, self) + if user.pbRaiseStatStage(s, 2, user, showAnim) + showAnim = false + end + end + end +end + +#=============================================================================== +# User gains 3/4 the HP it inflicts as damage. (Draining Kiss, Oblivion Wing) +#=============================================================================== +class PokeBattle_Move_14F < PokeBattle_Move + def healingMove? + return Settings::MECHANICS_GENERATION >= 6; + end + + def pbEffectAgainstTarget(user, target) + return if target.damageState.hpLost <= 0 + hpGain = (target.damageState.hpLost * 0.75).round + user.pbRecoverHPFromDrain(hpGain, target) + end +end + +#=============================================================================== +# If this move KO's the target, increases the user's Attack by 3 stages. +# (Fell Stinger) +#=============================================================================== +class PokeBattle_Move_150 < PokeBattle_Move + def pbEffectAfterAllHits(user, target) + return if !target.damageState.fainted + return if !user.pbCanRaiseStatStage?(:ATTACK, user, self) + user.pbRaiseStatStage(:ATTACK, 3, user) + end +end + +#=============================================================================== +# Decreases the target's Attack and Special Attack by 1 stage each. Then, user +# switches out. Ignores trapping moves. (Parting Shot) +#=============================================================================== +class PokeBattle_Move_151 < PokeBattle_TargetMultiStatDownMove + def initialize(battle, move) + super + @statDown = [:ATTACK, 1, :SPECIAL_ATTACK, 1] + end + + def pbEndOfMoveUsageEffect(user, targets, numHits, switchedBattlers) + switcher = user + targets.each do |b| + next if switchedBattlers.include?(b.index) + switcher = b if b.effects[PBEffects::MagicCoat] || b.effects[PBEffects::MagicBounce] + end + return if switcher.fainted? || numHits == 0 + return if !@battle.pbCanChooseNonActive?(switcher.index) + @battle.pbDisplay(_INTL("{1} went back to {2}!", switcher.pbThis, + @battle.pbGetOwnerName(switcher.index))) + @battle.pbPursuit(switcher.index) + return if switcher.fainted? + newPkmn = @battle.pbGetReplacementPokemonIndex(switcher.index) # Owner chooses + return if newPkmn < 0 + @battle.pbRecallAndReplace(switcher.index, newPkmn) + @battle.pbClearChoice(switcher.index) # Replacement Pokémon does nothing this round + @battle.moldBreaker = false if switcher.index == user.index + switchedBattlers.push(switcher.index) + switcher.pbEffectsOnSwitchIn(true) + end +end + +#=============================================================================== +# No Pokémon can switch out or flee until the end of the next round, as long as +# the user remains active. (Fairy Lock) +#=============================================================================== +class PokeBattle_Move_152 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.field.effects[PBEffects::FairyLock] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.field.effects[PBEffects::FairyLock] = 2 + @battle.pbDisplay(_INTL("No one will be able to run away during the next turn!")) + end +end + +#=============================================================================== +# Entry hazard. Lays stealth rocks on the opposing side. (Sticky Web) +#=============================================================================== +class PokeBattle_Move_153 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.pbOpposingSide.effects[PBEffects::StickyWeb] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOpposingSide.effects[PBEffects::StickyWeb] = true + @battle.pbDisplay(_INTL("A sticky web has been laid out beneath {1}'s feet!", + user.pbOpposingTeam(true))) + end +end + +#=============================================================================== +# For 5 rounds, creates an electric terrain which boosts Electric-type moves and +# prevents Pokémon from falling asleep. Affects non-airborne Pokémon only. +# (Electric Terrain) +#=============================================================================== +class PokeBattle_Move_154 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.field.terrain == :Electric + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.pbStartTerrain(user, :Electric) + end +end + +#=============================================================================== +# For 5 rounds, creates a grassy terrain which boosts Grass-type moves and heals +# Pokémon at the end of each round. Affects non-airborne Pokémon only. +# (Grassy Terrain) +#=============================================================================== +class PokeBattle_Move_155 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.field.terrain == :Grassy + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.pbStartTerrain(user, :Grassy) + end +end + +#=============================================================================== +# For 5 rounds, creates a misty terrain which weakens Dragon-type moves and +# protects Pokémon from status problems. Affects non-airborne Pokémon only. +# (Misty Terrain) +#=============================================================================== +class PokeBattle_Move_156 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.field.terrain == :Misty + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.pbStartTerrain(user, :Misty) + end +end + +#=============================================================================== +# Doubles the prize money the player gets after winning the battle. (Happy Hour) +#=============================================================================== +class PokeBattle_Move_157 < PokeBattle_Move + def pbEffectGeneral(user) + @battle.field.effects[PBEffects::HappyHour] = true if !user.opposes? + @battle.pbDisplay(_INTL("Everyone is caught up in the happy atmosphere!")) + end +end + +#=============================================================================== +# Fails unless user has consumed a berry at some point. (Belch) +#=============================================================================== +class PokeBattle_Move_158 < PokeBattle_Move + def pbCanChooseMove?(user, commandPhase, showMessages) + if !user.belched? + if showMessages + msg = _INTL("{1} hasn't eaten any held berry, so it can't possibly belch!", user.pbThis) + (commandPhase) ? @battle.pbDisplayPaused(msg) : @battle.pbDisplay(msg) + end + return false + end + return true + end + + def pbMoveFailed?(user, targets) + if !user.belched? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# Poisons the target and decreases its Speed by 1 stage. (Toxic Thread) +#=============================================================================== +class PokeBattle_Move_159 < PokeBattle_Move + def pbFailsAgainstTarget?(user, target) + if !target.pbCanPoison?(user, false, self) && + !target.pbCanLowerStatStage?(:SPEED, user, self) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.pbPoison(user) if target.pbCanPoison?(user, false, self) + if target.pbCanLowerStatStage?(:SPEED, user, self) + target.pbLowerStatStage(:SPEED, 1, user) + end + end +end + +#=============================================================================== +# Cures the target's burn. (Sparkling Aria) +#=============================================================================== +class PokeBattle_Move_15A < PokeBattle_Move + def pbAdditionalEffect(user, target) + return if target.fainted? || target.damageState.substitute + return if target.status != :BURN + target.pbCureStatus + end +end + +#=============================================================================== +# Cures the target's permanent status problems. Heals user by 1/2 of its max HP. +# (Purify) +#=============================================================================== +class PokeBattle_Move_15B < PokeBattle_HealingMove + def pbFailsAgainstTarget?(user, target) + if target.status == :NONE + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbHealAmount(user) + return (user.totalhp / 2.0).round + end + + def pbEffectAgainstTarget(user, target) + target.pbCureStatus + super + end +end + +#=============================================================================== +# Increases the user's and its ally's Attack and Special Attack by 1 stage each, +# if they have Plus or Minus. (Gear Up) +#=============================================================================== +# NOTE: In Gen 5, this move should have a target of UserSide, while in Gen 6+ it +# should have a target of UserAndAllies. This is because, in Gen 5, this +# move shouldn't call def pbSuccessCheckAgainstTarget for each Pokémon +# currently in battle that will be affected by this move (i.e. allies +# aren't protected by their substitute/ability/etc., but they are in Gen +# 6+). We achieve this by not targeting any battlers in Gen 5, since +# pbSuccessCheckAgainstTarget is only called for targeted battlers. +class PokeBattle_Move_15C < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbMoveFailed?(user, targets) + @validTargets = [] + @battle.eachSameSideBattler(user) do |b| + next if !b.hasActiveAbility?([:MINUS, :PLUS]) + next if !b.pbCanRaiseStatStage?(:ATTACK, user, self) && + !b.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) + @validTargets.push(b) + end + if @validTargets.length == 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbFailsAgainstTarget?(user, target) + return false if @validTargets.any? { |b| b.index == target.index } + return true if !target.hasActiveAbility?([:MINUS, :PLUS]) + @battle.pbDisplay(_INTL("{1}'s stats can't be raised further!", target.pbThis)) + return true + end + + def pbEffectAgainstTarget(user, target) + showAnim = true + if target.pbCanRaiseStatStage?(:ATTACK, user, self) + if target.pbRaiseStatStage(:ATTACK, 1, user, showAnim) + showAnim = false + end + end + if target.pbCanRaiseStatStage?(:SPECIAL_ATTACK, user, self) + target.pbRaiseStatStage(:SPECIAL_ATTACK, 1, user, showAnim) + end + end + + def pbEffectGeneral(user) + return if pbTarget(user) != :UserSide + @validTargets.each { |b| pbEffectAgainstTarget(user, b) } + end +end + +#=============================================================================== +# User gains stat stages equal to each of the target's positive stat stages, +# and target's positive stat stages become 0, before damage calculation. +# (Spectral Thief) +#=============================================================================== +class PokeBattle_Move_15D < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbCalcDamage(user, target, numTargets = 1) + if target.hasRaisedStatStages? + pbShowAnimation(@id, user, target, 1) # Stat stage-draining animation + @battle.pbDisplay(_INTL("{1} stole the target's boosted stats!", user.pbThis)) + showAnim = true + GameData::Stat.each_battle do |s| + next if target.stages[s.id] <= 0 + if user.pbCanRaiseStatStage?(s.id, user, self) + if user.pbRaiseStatStage(s.id, target.stages[s.id], user, showAnim) + showAnim = false + end + end + target.stages[s.id] = 0 + end + end + super + end +end + +#=============================================================================== +# Until the end of the next round, the user's moves will always be critical hits. +# (Laser Focus) +#=============================================================================== +class PokeBattle_Move_15E < PokeBattle_Move + def pbEffectGeneral(user) + user.effects[PBEffects::LaserFocus] = 2 + @battle.pbDisplay(_INTL("{1} concentrated intensely!", user.pbThis)) + end +end + +#=============================================================================== +# Decreases the user's Defense by 1 stage. (Clanging Scales) +#=============================================================================== +class PokeBattle_Move_15F < PokeBattle_StatDownMove + def initialize(battle, move) + super + @statDown = [:DEFENSE, 1] + end +end + +#=============================================================================== +# Decreases the target's Attack by 1 stage. Heals user by an amount equal to the +# target's Attack stat (after applying stat stages, before this move decreases +# it). (Strength Sap) +#=============================================================================== +class PokeBattle_Move_160 < PokeBattle_Move + def healingMove? + return true; + end + + def pbFailsAgainstTarget?(user, target) + # NOTE: The official games appear to just check whether the target's Attack + # stat stage is -6 and fail if so, but I've added the "fail if target + # has Contrary and is at +6" check too for symmetry. This move still + # works even if the stat stage cannot be changed due to an ability or + # other effect. + if !@battle.moldBreaker && target.hasActiveAbility?(:CONTRARY) && + target.statStageAtMax?(:ATTACK) + @battle.pbDisplay(_INTL("But it failed!")) + return true + elsif target.statStageAtMin?(:ATTACK) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + # Calculate target's effective attack value + stageMul = [2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8] + stageDiv = [8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2] + atk = target.attack + atkStage = target.stages[:ATTACK] + 6 + healAmt = (atk.to_f * stageMul[atkStage] / stageDiv[atkStage]).floor + # Reduce target's Attack stat + if target.pbCanLowerStatStage?(:ATTACK, user, self) + target.pbLowerStatStage(:ATTACK, 1, user) + end + # Heal user + if target.hasActiveAbility?(:LIQUIDOOZE) + @battle.pbShowAbilitySplash(target) + user.pbReduceHP(healAmt) + @battle.pbDisplay(_INTL("{1} sucked up the liquid ooze!", user.pbThis)) + @battle.pbHideAbilitySplash(target) + user.pbItemHPHealCheck + elsif user.canHeal? + healAmt = (healAmt * 1.3).floor if user.hasActiveItem?(:BIGROOT) + user.pbRecoverHP(healAmt) + @battle.pbDisplay(_INTL("{1}'s HP was restored.", user.pbThis)) + end + end +end + +#=============================================================================== +# User and target swap their Speed stats (not their stat stages). (Speed Swap) +#=============================================================================== +class PokeBattle_Move_161 < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def pbEffectAgainstTarget(user, target) + user.speed, target.speed = target.speed, user.speed + @battle.pbDisplay(_INTL("{1} switched Speed with its target!", user.pbThis)) + end +end + +#=============================================================================== +# User loses their Fire type. Fails if user is not Fire-type. (Burn Up) +#=============================================================================== +class PokeBattle_Move_162 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if !user.pbHasType?(:FIRE) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAfterAllHits(user, target) + if !user.effects[PBEffects::BurnUp] + user.effects[PBEffects::BurnUp] = true + @battle.pbDisplay(_INTL("{1} burned itself out!", user.pbThis)) + end + end +end + +#=============================================================================== +# Ignores all abilities that alter this move's success or damage. +# (Moongeist Beam, Sunsteel Strike) +#=============================================================================== +class PokeBattle_Move_163 < PokeBattle_Move + def pbChangeUsageCounters(user, specialUsage) + super + @battle.moldBreaker = true if !specialUsage + end +end + +#=============================================================================== +# Ignores all abilities that alter this move's success or damage. This move is +# physical if user's Attack is higher than its Special Attack (after applying +# stat stages), and special otherwise. (Photon Geyser) +#=============================================================================== +class PokeBattle_Move_164 < PokeBattle_Move_163 + def initialize(battle, move) + super + @calcCategory = 1 + end + + def physicalMove?(thisType = nil) + ; return (@calcCategory == 0); + end + + def specialMove?(thisType = nil) + ; return (@calcCategory == 1); + end + + def pbOnStartUse(user, targets) + echoln _INTL("type1={1}",user.type1) + echoln _INTL("type2={1}",user.type2) + + # Calculate user's effective attacking value + stageMul = [2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8] + stageDiv = [8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2] + atk = user.attack + atkStage = user.stages[:ATTACK] + 6 + realAtk = (atk.to_f * stageMul[atkStage] / stageDiv[atkStage]).floor + spAtk = user.spatk + spAtkStage = user.stages[:SPECIAL_ATTACK] + 6 + realSpAtk = (spAtk.to_f * stageMul[spAtkStage] / stageDiv[spAtkStage]).floor + # Determine move's category + @calcCategory = (realAtk > realSpAtk) ? 0 : 1 + + if user.hasActiveItem?(:NECROZIUM) && (user.isFusionOf(:NECROZMA) && !user.pokemon.spriteform_body && !user.pokemon.spriteform_head) + # user.type2 = :DRAGON if user.pokemon.hasBodyOf?(:NECROZMA) + # user.attack*=1.3 + # user.spatk*=1.3 + # user.defense*=0.5 + # user.spdef*=0.5 + # user.speed*=1.5 + + user.changeFormSpecies(:NECROZMA,:U_NECROZMA,"UltraBurst2") + #user.changeForm(1,:NECROZMA) + end + end +end + +#change back at the end of battle +Events.onEndBattle += proc { |_sender,_e| + $Trainer.party.each_with_index do |value, i| + pokemon = $Trainer.party[i] + pokemon.changeFormSpecies(:U_NECROZMA,:NECROZMA) if pokemon.isFusionOf(:U_NECROZMA) + end +} + +#=============================================================================== +# Negates the target's ability while it remains on the field, if it has already +# performed its action this round. (Core Enforcer) +#=============================================================================== +class PokeBattle_Move_165 < PokeBattle_Move + def pbEffectAgainstTarget(user, target) + return if target.damageState.substitute || target.effects[PBEffects::GastroAcid] + return if target.unstoppableAbility? + return if @battle.choices[target.index][0] != :UseItem && + !((@battle.choices[target.index][0] == :UseMove || + @battle.choices[target.index][0] == :Shift) && target.movedThisRound?) + target.effects[PBEffects::GastroAcid] = true + target.effects[PBEffects::Truant] = false + @battle.pbDisplay(_INTL("{1}'s Ability was suppressed!", target.pbThis)) + target.pbOnAbilityChanged(target.ability) + end +end + +#=============================================================================== +# Power is doubled if the user's last move failed. (Stomping Tantrum) +#=============================================================================== +class PokeBattle_Move_166 < PokeBattle_Move + def pbBaseDamage(baseDmg, user, target) + baseDmg *= 2 if user.lastRoundMoveFailed + return baseDmg + end +end + +#=============================================================================== +# For 5 rounds, lowers power of attacks against the user's side. Fails if +# weather is not hail. (Aurora Veil) +#=============================================================================== +class PokeBattle_Move_167 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.pbWeather != :Hail + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if user.pbOwnSide.effects[PBEffects::AuroraVeil] > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + user.pbOwnSide.effects[PBEffects::AuroraVeil] = 5 + user.pbOwnSide.effects[PBEffects::AuroraVeil] = 8 if user.hasActiveItem?(:LIGHTCLAY) + @battle.pbDisplay(_INTL("{1} made {2} stronger against physical and special moves!", + @name, user.pbTeam(true))) + end +end + +#=============================================================================== +# User is protected against moves with the "B" flag this round. If a Pokémon +# makes contact with the user while this effect applies, that Pokémon is +# poisoned. (Baneful Bunker) +#=============================================================================== +class PokeBattle_Move_168 < PokeBattle_ProtectMove + def initialize(battle, move) + super + @effect = PBEffects::BanefulBunker + end +end + +#=============================================================================== +# This move's type is the same as the user's first type. (Revelation Dance) +#=============================================================================== +class PokeBattle_Move_169 < PokeBattle_Move + def pbBaseType(user) + userTypes = user.pbTypes(true) + return userTypes[0] + end +end + +#=============================================================================== +# This round, target becomes the target of attacks that have single targets. +# (Spotlight) +#=============================================================================== +class PokeBattle_Move_16A < PokeBattle_Move + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Spotlight] = 1 + target.eachAlly do |b| + next if b.effects[PBEffects::Spotlight] < target.effects[PBEffects::Spotlight] + target.effects[PBEffects::Spotlight] = b.effects[PBEffects::Spotlight] + 1 + end + @battle.pbDisplay(_INTL("{1} became the center of attention!", target.pbThis)) + end +end + +#=============================================================================== +# The target uses its most recent move again. (Instruct) +#=============================================================================== +class PokeBattle_Move_16B < PokeBattle_Move + def ignoresSubstitute?(user) + ; return true; + end + + def initialize(battle, move) + super + @moveBlacklist = [ + "0D4", # Bide + "14B", # King's Shield + "16B", # Instruct (this move) + # Struggle + "002", # Struggle + # Moves that affect the moveset + "05C", # Mimic + "05D", # Sketch + "069", # Transform + # Moves that call other moves + "0AE", # Mirror Move + "0AF", # Copycat + "0B0", # Me First + "0B3", # Nature Power + "0B4", # Sleep Talk + "0B5", # Assist + "0B6", # Metronome + # Moves that require a recharge turn + "0C2", # Hyper Beam + # Two-turn attacks + "0C3", # Razor Wind + "0C4", # Solar Beam, Solar Blade + "0C5", # Freeze Shock + "0C6", # Ice Burn + "0C7", # Sky Attack + "0C8", # Skull Bash + "0C9", # Fly + "0CA", # Dig + "0CB", # Dive + "0CC", # Bounce + "0CD", # Shadow Force + "0CE", # Sky Drop + "12E", # Shadow Half + "14D", # Phantom Force + "14E", # Geomancy + # Moves that start focussing at the start of the round + "115", # Focus Punch + "171", # Shell Trap + "172" # Beak Blast + ] + end + + def pbFailsAgainstTarget?(user, target) + if !target.lastRegularMoveUsed || !target.pbHasMove?(target.lastRegularMoveUsed) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if target.usingMultiTurnAttack? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + targetMove = @battle.choices[target.index][2] + if targetMove && (targetMove.function == "115" || # Focus Punch + targetMove.function == "171" || # Shell Trap + targetMove.function == "172") # Beak Blast + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if @moveBlacklist.include?(GameData::Move.get(target.lastRegularMoveUsed).function_code) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + idxMove = -1 + target.eachMoveWithIndex do |m, i| + idxMove = i if m.id == target.lastRegularMoveUsed + end + if target.moves[idxMove].pp == 0 && target.moves[idxMove].total_pp > 0 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + target.effects[PBEffects::Instruct] = true + end +end + +#=============================================================================== +# Target cannot use sound-based moves for 2 more rounds. (Throat Chop) +#=============================================================================== +class PokeBattle_Move_16C < PokeBattle_Move + def pbAdditionalEffect(user, target) + return if target.fainted? || target.damageState.substitute + @battle.pbDisplay(_INTL("The effects of {1} prevent {2} from using certain moves!", + @name, target.pbThis(true))) if target.effects[PBEffects::ThroatChop] == 0 + target.effects[PBEffects::ThroatChop] = 3 + end +end + +#=============================================================================== +# Heals user by 1/2 of its max HP, or 2/3 of its max HP in a sandstorm. (Shore Up) +#=============================================================================== +class PokeBattle_Move_16D < PokeBattle_HealingMove + def pbHealAmount(user) + return (user.totalhp * 2 / 3.0).round if @battle.pbWeather == :Sandstorm + return (user.totalhp / 2.0).round + end +end + +#=============================================================================== +# Heals target by 1/2 of its max HP, or 2/3 of its max HP in Grassy Terrain. +# (Floral Healing) +#=============================================================================== +class PokeBattle_Move_16E < PokeBattle_Move + def healingMove? + return true; + end + + def pbFailsAgainstTarget?(user, target) + if target.hp == target.totalhp + @battle.pbDisplay(_INTL("{1}'s HP is full!", target.pbThis)) + return true + elsif !target.canHeal? + @battle.pbDisplay(_INTL("{1} is unaffected!", target.pbThis)) + return true + end + return false + end + + def pbEffectAgainstTarget(user, target) + hpGain = (target.totalhp / 2.0).round + hpGain = (target.totalhp * 2 / 3.0).round if @battle.field.terrain == :Grassy + target.pbRecoverHP(hpGain) + @battle.pbDisplay(_INTL("{1}'s HP was restored.", target.pbThis)) + end +end + +#=============================================================================== +# Damages target if target is a foe, or heals target by 1/2 of its max HP if +# target is an ally. (Pollen Puff) +#=============================================================================== +class PokeBattle_Move_16F < PokeBattle_Move + def pbTarget(user) + return GameData::Target.get(:NearFoe) if user.effects[PBEffects::HealBlock] > 0 + return super + end + + def pbOnStartUse(user, targets) + @healing = false + @healing = !user.opposes?(targets[0]) if targets.length > 0 + end + + def pbFailsAgainstTarget?(user, target) + return false if !@healing + if target.effects[PBEffects::Substitute] > 0 && !ignoresSubstitute?(user) + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if !target.canHeal? + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbDamagingMove? + return false if @healing + return super + end + + def pbEffectAgainstTarget(user, target) + return if !@healing + target.pbRecoverHP(target.totalhp / 2) + @battle.pbDisplay(_INTL("{1}'s HP was restored.", target.pbThis)) + end + + def pbShowAnimation(id, user, targets, hitNum = 0, showAnimation = true) + hitNum = 1 if @healing # Healing anim + super + end +end + +#=============================================================================== +# Damages user by 1/2 of its max HP, even if this move misses. (Mind Blown) +#=============================================================================== +class PokeBattle_Move_170 < PokeBattle_Move + def worksWithNoTargets? + return true; + end + + def pbMoveFailed?(user, targets) + if !@battle.moldBreaker + bearer = @battle.pbCheckGlobalAbility(:DAMP) + if bearer != nil + @battle.pbShowAbilitySplash(bearer) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @battle.pbDisplay(_INTL("{1} cannot use {2}!", user.pbThis, @name)) + else + @battle.pbDisplay(_INTL("{1} cannot use {2} because of {3}'s {4}!", + user.pbThis, @name, bearer.pbThis(true), bearer.abilityName)) + end + @battle.pbHideAbilitySplash(bearer) + return true + end + end + return false + end + + def pbSelfKO(user) + return if !user.takesIndirectDamage? + user.pbReduceHP((user.totalhp / 2.0).round, false) + user.pbItemHPHealCheck + end +end + +#=============================================================================== +# Fails if user has not been hit by an opponent's physical move this round. +# (Shell Trap) +#=============================================================================== +class PokeBattle_Move_171 < PokeBattle_Move + def pbDisplayChargeMessage(user) + user.effects[PBEffects::ShellTrap] = true + @battle.pbCommonAnimation("ShellTrap", user) + @battle.pbDisplay(_INTL("{1} set a shell trap!", user.pbThis)) + end + + def pbDisplayUseMessage(user) + super if user.tookPhysicalHit + end + + def pbMoveFailed?(user, targets) + if !user.effects[PBEffects::ShellTrap] + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + if !user.tookPhysicalHit + @battle.pbDisplay(_INTL("{1}'s shell trap didn't work!", user.pbThis)) + return true + end + return false + end +end + +#=============================================================================== +# If a Pokémon makes contact with the user before it uses this move, the +# attacker is burned. (Beak Blast) +#=============================================================================== +class PokeBattle_Move_172 < PokeBattle_Move + def pbDisplayChargeMessage(user) + user.effects[PBEffects::BeakBlast] = true + @battle.pbCommonAnimation("BeakBlast", user) + @battle.pbDisplay(_INTL("{1} started heating up its beak!", user.pbThis)) + end +end + +#=============================================================================== +# For 5 rounds, creates a psychic terrain which boosts Psychic-type moves and +# prevents Pokémon from being hit by >0 priority moves. Affects non-airborne +# Pokémon only. (Psychic Terrain) +#=============================================================================== +class PokeBattle_Move_173 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if @battle.field.terrain == :Psychic + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end + + def pbEffectGeneral(user) + @battle.pbStartTerrain(user, :Psychic) + end +end + +#=============================================================================== +# Fails if this isn't the user's first turn. (First Impression) +#=============================================================================== +class PokeBattle_Move_174 < PokeBattle_Move + def pbMoveFailed?(user, targets) + if user.turnCount > 1 + @battle.pbDisplay(_INTL("But it failed!")) + return true + end + return false + end +end + +#=============================================================================== +# Hits twice. Causes the target to flinch. Does double damage and has perfect +# accuracy if the target is Minimized. (Double Iron Bash) +#=============================================================================== +class PokeBattle_Move_175 < PokeBattle_FlinchMove + def multiHitMove? + return true; + end + + def pbNumHits(user, targets) + ; return 2; + end + + def tramplesMinimize?(param = 1) + ; return true; + end +end + +# NOTE: If you're inventing new move effects, use function code 176 and onwards. +# Actually, you might as well use high numbers like 500+ (up to FFFF), +# just to make sure later additions to Essentials don't clash with your +# new effects. diff --git a/Data/Scripts/011_Battle/003_Battle/001_PokeBattle_BattleCommon.rb b/Data/Scripts/011_Battle/003_Battle/001_PokeBattle_BattleCommon.rb new file mode 100644 index 000000000..1fc2b4994 --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/001_PokeBattle_BattleCommon.rb @@ -0,0 +1,264 @@ +module PokeBattle_BattleCommon + #============================================================================= + # Store caught Pokémon + #============================================================================= + def pbStorePokemon(pkmn) + # Nickname the Pokémon (unless it's a Shadow Pokémon) + if !pkmn.shadowPokemon? + if pbDisplayConfirm(_INTL("Would you like to give a nickname to {1}?", pkmn.name)) + nickname = @scene.pbNameEntry(_INTL("{1}'s nickname?", pkmn.speciesName), pkmn) + pkmn.name = nickname + end + end + # Store the Pokémon + currentBox = @peer.pbCurrentBox + storedBox = @peer.pbStorePokemon(pbPlayer, pkmn) + if storedBox < 0 + pbDisplayPaused(_INTL("{1} has been added to your party.", pkmn.name)) + @initialItems[0][pbPlayer.party.length - 1] = pkmn.item_id if @initialItems + return + end + # Messages saying the Pokémon was stored in a PC box + creator = @peer.pbGetStorageCreatorName + curBoxName = @peer.pbBoxName(currentBox) + boxName = @peer.pbBoxName(storedBox) + if storedBox != currentBox + if creator + pbDisplayPaused(_INTL("Box \"{1}\" on {2}'s PC was full.", curBoxName, creator)) + else + pbDisplayPaused(_INTL("Box \"{1}\" on someone's PC was full.", curBoxName)) + end + pbDisplayPaused(_INTL("{1} was transferred to box \"{2}\".", pkmn.name, boxName)) + else + if creator + pbDisplayPaused(_INTL("{1} was transferred to {2}'s PC.", pkmn.name, creator)) + else + pbDisplayPaused(_INTL("{1} was transferred to someone's PC.", pkmn.name)) + end + pbDisplayPaused(_INTL("It was stored in box \"{1}\".", boxName)) + end + end + + #def pbChoosePokemon(variableNumber, nameVarNumber, ableProc = nil, allowIneligible = false) + # def swapCaughtPokemon(caughtPokemon) + # pbChoosePokemon(1,2, + # proc {|poke| + # !poke.egg? && + # !(poke.isShadow? rescue false) + # }) + # index = pbGet(1) + # return false if index == -1 + # $PokemonStorage.pbStoreCaught($Trainer.party[index]) + # pbRemovePokemonAt(index) + # pbStorePokemon(caughtPokemon) + # return true + # end + + # Register all caught Pokémon in the Pokédex, and store them. + def pbRecordAndStoreCaughtPokemon + @caughtPokemon.each do |pkmn| + pbPlayer.pokedex.register(pkmn) # In case the form changed upon leaving battle + # Record the Pokémon's species as owned in the Pokédex + if !pbPlayer.owned?(pkmn.species) + pbPlayer.pokedex.set_owned(pkmn.species) + if $Trainer.has_pokedex + pbDisplayPaused(_INTL("{1}'s data was added to the Pokédex.", pkmn.name)) + pbPlayer.pokedex.register_last_seen(pkmn) + @scene.pbShowPokedex(pkmn.species) + end + end + # Record a Shadow Pokémon's species as having been caught + pbPlayer.pokedex.set_shadow_pokemon_owned(pkmn.species) if pkmn.shadowPokemon? + # Store caught Pokémon + promptCaughtPokemonAction(pkmn) + if $game_switches[AUTOSAVE_CATCH_SWITCH] + Kernel.tryAutosave() + end + + end + @caughtPokemon.clear + end + + # def promptCaughtPokemonAction(pokemon) + # pickedOption = false + # return pbStorePokemon(pokemon) if !$Trainer.party_full? + # + # while !pickedOption + # command = pbMessage(_INTL("\\ts[]Your team is full!"), + # [_INTL("Add to your party"), _INTL("Store to PC"),], 2) + # echoln ("command " + command.to_s) + # case command + # when 0 #SWAP + # if swapCaughtPokemon(pokemon) + # echoln pickedOption + # pickedOption = true + # end + # else + # #STORE + # pbStorePokemon(pokemon) + # echoln pickedOption + # pickedOption = true + # end + # end + # + # end + + #============================================================================= + # Throw a Poké Ball + #============================================================================= + def pbThrowPokeBall(idxBattler, ball, catch_rate = nil, showPlayer = false) + # Determine which Pokémon you're throwing the Poké Ball at + battler = nil + if opposes?(idxBattler) + battler = @battlers[idxBattler] + else + battler = @battlers[idxBattler].pbDirectOpposing(true) + end + if battler.fainted? + battler.eachAlly do |b| + battler = b + break + end + end + # Messages + itemName = GameData::Item.get(ball).name + if battler.fainted? + if itemName.starts_with_vowel? + pbDisplay(_INTL("{1} threw an {2}!", pbPlayer.name, itemName)) + else + pbDisplay(_INTL("{1} threw a {2}!", pbPlayer.name, itemName)) + end + pbDisplay(_INTL("But there was no target...")) + return + end + if itemName.starts_with_vowel? + pbDisplayBrief(_INTL("{1} threw an {2}!", pbPlayer.name, itemName)) + else + pbDisplayBrief(_INTL("{1} threw a {2}!", pbPlayer.name, itemName)) + end + # Animation of opposing trainer blocking Poké Balls (unless it's a Snag Ball + # at a Shadow Pokémon) + if trainerBattle? && !(GameData::Item.get(ball).is_snag_ball? && battler.shadowPokemon?) + @scene.pbThrowAndDeflect(ball, 1) + pbDisplay(_INTL("The Trainer blocked your Poké Ball! Don't be a thief!")) + return + elsif $game_switches[SWITCH_CANNOT_CATCH_POKEMON] + @scene.pbThrowAndDeflect(ball, 1) + pbDisplay(_INTL("The Pokémon is impossible to catch!")) + return + end + # Calculate the number of shakes (4=capture) + pkmn = battler.pokemon + @criticalCapture = false + numShakes = pbCaptureCalc(pkmn, battler, catch_rate, ball) + PBDebug.log("[Threw Poké Ball] #{itemName}, #{numShakes} shakes (4=capture)") + # Animation of Ball throw, absorb, shake and capture/burst out + @scene.pbThrow(ball, numShakes, @criticalCapture, battler.index, showPlayer) + # Outcome message + case numShakes + when 0 + pbDisplay(_INTL("Oh no! The Pokémon broke free!")) + BallHandlers.onFailCatch(ball, self, battler) + when 1 + pbDisplay(_INTL("Aww! It appeared to be caught!")) + BallHandlers.onFailCatch(ball, self, battler) + when 2 + pbDisplay(_INTL("Aargh! Almost had it!")) + BallHandlers.onFailCatch(ball, self, battler) + when 3 + pbDisplay(_INTL("Gah! It was so close, too!")) + BallHandlers.onFailCatch(ball, self, battler) + when 4 + if $game_switches[SWITCH_SILVERBOSS_BATTLE] + pkmn.species = :PALDIATINA + pkmn.name = "Paldiatina" + end + pbDisplayBrief(_INTL("Gotcha! {1} was caught!", pkmn.name)) + @scene.pbThrowSuccess # Play capture success jingle + pbRemoveFromParty(battler.index, battler.pokemonIndex) + # Gain Exp + if Settings::GAIN_EXP_FOR_CAPTURE + battler.captured = true + pbGainExp + battler.captured = false + end + battler.pbReset + if pbAllFainted?(battler.index) + @decision = (trainerBattle?) ? 1 : 4 # Battle ended by win/capture + end + # Modify the Pokémon's properties because of the capture + if GameData::Item.get(ball).is_snag_ball? + pkmn.owner = Pokemon::Owner.new_from_trainer(pbPlayer) + end + BallHandlers.onCatch(ball, self, pkmn) + pkmn.poke_ball = ball + pkmn.makeUnmega if pkmn.mega? + pkmn.makeUnprimal + pkmn.update_shadow_moves if pkmn.shadowPokemon? + pkmn.record_first_moves + # Reset form + pkmn.forced_form = nil if MultipleForms.hasFunction?(pkmn.species, "getForm") + @peer.pbOnLeavingBattle(self, pkmn, true, true) + # Make the Poké Ball and data box disappear + @scene.pbHideCaptureBall(idxBattler) + # Save the Pokémon for storage at the end of battle + @caughtPokemon.push(pkmn) + end + end + + #============================================================================= + # Calculate how many shakes a thrown Poké Ball will make (4 = capture) + #============================================================================= + def pbCaptureCalc(pkmn, battler, catch_rate, ball) + return 4 if $DEBUG && Input.press?(Input::CTRL) + # Get a catch rate if one wasn't provided + catch_rate = pkmn.species_data.catch_rate if !catch_rate + # Modify catch_rate depending on the Poké Ball's effect + ultraBeast = [:NIHILEGO, :BUZZWOLE, :PHEROMOSA, :XURKITREE, :CELESTEELA, + :KARTANA, :GUZZLORD, :POIPOLE, :NAGANADEL, :STAKATAKA, + :BLACEPHALON].include?(pkmn.species) + if !ultraBeast || ball == :BEASTBALL + catch_rate = BallHandlers.modifyCatchRate(ball, catch_rate, self, battler, ultraBeast) + else + catch_rate /= 10 + end + + + + # First half of the shakes calculation + a = battler.totalhp + b = battler.hp + x = ((3 * a - 2 * b) * catch_rate.to_f) / (3 * a) + # Calculation modifiers + if battler.status == :SLEEP || battler.status == :FROZEN + x *= 2.5 + elsif battler.status != :NONE + x *= 1.5 + end + x = x.floor + x = 1 if x < 1 + # Definite capture, no need to perform randomness checks + return 4 if x >= 255 || BallHandlers.isUnconditional?(ball, self, battler) + # Second half of the shakes calculation + y = (65536 / ((255.0 / x) ** 0.1875)).floor + + #Increased chances of catching if is on last ball + isOnLastBall = !$PokemonBag.pbHasItem?(ball) + echoln isOnLastBall + # Critical capture check + if isOnLastBall + c = x * 6 / 12 + if c > 0 && pbRandom(256) < c + @criticalCapture = true + return 4 + end + end + # Calculate the number of shakes + numShakes = 0 + for i in 0...4 + break if numShakes < i + numShakes += 1 if pbRandom(65536) < y + end + return numShakes + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/002_PokeBattle_Battle.rb b/Data/Scripts/011_Battle/003_Battle/002_PokeBattle_Battle.rb new file mode 100644 index 000000000..56708268b --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/002_PokeBattle_Battle.rb @@ -0,0 +1,799 @@ +# Results of battle: +# 0 - Undecided or aborted +# 1 - Player won +# 2 - Player lost +# 3 - Player or wild Pokémon ran from battle, or player forfeited the match +# 4 - Wild Pokémon was caught +# 5 - Draw +# Possible actions a battler can take in a round: +# :None +# :UseMove +# :SwitchOut +# :UseItem +# :Call +# :Run +# :Shift +# NOTE: If you want to have more than 3 Pokémon on a side at once, you will need +# to edit some code. Mainly this is to change/add coordinates for the +# sprites, describe the relationships between Pokémon and trainers, and to +# change messages. The methods that will need editing are as follows: +# class PokeBattle_Battle +# def setBattleMode +# def pbGetOwnerIndexFromBattlerIndex +# def pbGetOpposingIndicesInOrder +# def nearBattlers? +# def pbStartBattleSendOut +# def pbEORShiftDistantBattlers +# def pbCanShift? +# def pbEndOfRoundPhase +# class TargetMenuDisplay +# def initialize +# class PokemonDataBox +# def initializeDataBoxGraphic +# module PokeBattle_SceneConstants +# def self.pbBattlerPosition +# def self.pbTrainerPosition +# class PokemonTemp +# def recordBattleRule +# (There is no guarantee that this list is complete.) + +class PokeBattle_Battle + attr_reader :scene # Scene object for this battle + attr_reader :peer + attr_reader :field # Effects common to the whole of a battle + attr_reader :sides # Effects common to each side of a battle + attr_reader :positions # Effects that apply to a battler position + attr_reader :battlers # Currently active Pokémon + attr_reader :sideSizes # Array of number of battlers per side + attr_accessor :backdrop # Filename fragment used for background graphics + attr_accessor :backdropBase # Filename fragment used for base graphics + attr_accessor :time # Time of day (0=day, 1=eve, 2=night) + attr_accessor :environment # Battle surroundings (for mechanics purposes) + attr_reader :turnCount + attr_accessor :decision # Decision: 0=undecided; 1=win; 2=loss; 3=escaped; 4=caught + attr_reader :player # Player trainer (or array of trainers) + attr_reader :opponent # Opponent trainer (or array of trainers) + attr_accessor :items # Items held by opponents + attr_accessor :endSpeeches + attr_accessor :endSpeechesWin + attr_accessor :party1starts # Array of start indexes for each player-side trainer's party + attr_accessor :party2starts # Array of start indexes for each opponent-side trainer's party + attr_accessor :internalBattle # Internal battle flag + attr_accessor :debug # Debug flag + attr_accessor :canRun # True if player can run from battle + attr_accessor :canLose # True if player won't black out if they lose + attr_accessor :switchStyle # Switch/Set "battle style" option + attr_accessor :showAnims # "Battle Effects" option + attr_accessor :controlPlayer # Whether player's Pokémon are AI controlled + attr_accessor :expGain # Whether Pokémon can gain Exp/EVs + attr_accessor :moneyGain # Whether the player can gain/lose money + attr_accessor :rules + attr_accessor :choices # Choices made by each Pokémon this round + attr_accessor :megaEvolution # Battle index of each trainer's Pokémon to Mega Evolve + attr_reader :initialItems + attr_reader :recycleItems + attr_reader :belch + attr_reader :battleBond + attr_reader :usedInBattle # Whether each Pokémon was used in battle (for Burmy) + attr_reader :successStates # Success states + attr_accessor :lastMoveUsed # Last move used + attr_accessor :lastMoveUser # Last move user + attr_reader :switching # True if during the switching phase of the round + attr_reader :futureSight # True if Future Sight is hitting + attr_reader :endOfRound # True during the end of round + attr_accessor :moldBreaker # True if Mold Breaker applies + attr_reader :struggle # The Struggle move + + include PokeBattle_BattleCommon + + def pbRandom(x); return rand(x); end + + #============================================================================= + # Creating the battle class + #============================================================================= + def initialize(scene,p1,p2,player,opponent) + if p1.length==0 + raise ArgumentError.new(_INTL("Party 1 has no Pokémon.")) + elsif p2.length==0 + raise ArgumentError.new(_INTL("Party 2 has no Pokémon.")) + end + @scene = scene + @peer = PokeBattle_BattlePeer.create + @battleAI = PokeBattle_AI.new(self) + @field = PokeBattle_ActiveField.new # Whole field (gravity/rooms) + @sides = [PokeBattle_ActiveSide.new, # Player's side + PokeBattle_ActiveSide.new] # Foe's side + @positions = [] # Battler positions + @battlers = [] + @sideSizes = [1,1] # Single battle, 1v1 + @backdrop = "" + @backdropBase = nil + @time = 0 + @environment = :None # e.g. Tall grass, cave, still water + @turnCount = 0 + @decision = 0 + @caughtPokemon = [] + player = [player] if !player.nil? && !player.is_a?(Array) + opponent = [opponent] if !opponent.nil? && !opponent.is_a?(Array) + @player = player # Array of Player/NPCTrainer objects, or nil + @opponent = opponent # Array of NPCTrainer objects, or nil + @items = nil + @endSpeeches = [] + @endSpeechesWin = [] + @party1 = p1 + @party2 = p2 + @party1order = Array.new(@party1.length) { |i| i } + @party2order = Array.new(@party2.length) { |i| i } + @party1starts = [0] + @party2starts = [0] + @internalBattle = true + @debug = false + @canRun = true + @canLose = false + @switchStyle = true + @showAnims = true + @controlPlayer = false + @expGain = true + @moneyGain = true + @rules = {} + @priority = [] + @priorityTrickRoom = false + @choices = [] + @megaEvolution = [ + [-1] * (@player ? @player.length : 1), + [-1] * (@opponent ? @opponent.length : 1) + ] + @initialItems = [ + Array.new(@party1.length) { |i| (@party1[i]) ? @party1[i].item_id : nil }, + Array.new(@party2.length) { |i| (@party2[i]) ? @party2[i].item_id : nil } + ] + @recycleItems = [Array.new(@party1.length, nil), Array.new(@party2.length, nil)] + @belch = [Array.new(@party1.length, false), Array.new(@party2.length, false)] + @battleBond = [Array.new(@party1.length, false), Array.new(@party2.length, false)] + @usedInBattle = [Array.new(@party1.length, false), Array.new(@party2.length, false)] + @successStates = [] + @lastMoveUsed = nil + @lastMoveUser = -1 + @switching = false + @futureSight = false + @endOfRound = false + @moldBreaker = false + @runCommand = 0 + @nextPickupUse = 0 + if GameData::Move.exists?(:STRUGGLE) + @struggle = PokeBattle_Move.from_pokemon_move(self, Pokemon::Move.new(:STRUGGLE)) + else + @struggle = PokeBattle_Struggle.new(self, nil) + end + end + + #============================================================================= + # Information about the type and size of the battle + #============================================================================= + def wildBattle?; return @opponent.nil?; end + def trainerBattle?; return !@opponent.nil?; end + + def get_default_battle_format() + case $PokemonSystem.battle_type + when 0 then return [1, 1] + when 1 then return [2, 2] + when 2 then return [3, 3] + end + return [1,1] + end + + # Sets the number of battler slots on each side of the field independently. + # For "1v2" names, the first number is for the player's side and the second + # number is for the opposing side. + def setBattleMode(mode) + default = get_default_battle_format() + @sideSizes = + case mode + when "triple", "3v3" then [3, 3] + when "3v2" then [3, 2] + when "3v1" then [3, 1] + when "2v3" then [2, 3] + when "double", "2v2" then [2, 2] + when "2v1" then [2, 1] + when "1v3" then [1, 3] + when "1v2" then [1, 2] + when "single" then [1, 1] + when "1v1" then [1, 1] + else default # Single, 1v1 (default) + end + end + + def singleBattle? + return pbSideSize(0)==1 && pbSideSize(1)==1 + end + + def pbSideSize(index) + return @sideSizes[index%2] + end + + def maxBattlerIndex + return (pbSideSize(0)>pbSideSize(1)) ? (pbSideSize(0)-1)*2 : pbSideSize(1)*2-1 + end + + #============================================================================= + # Trainers and owner-related methods + #============================================================================= + def pbPlayer; return @player[0]; end + + # Given a battler index, returns the index within @player/@opponent of the + # trainer that controls that battler index. + # NOTE: You shouldn't ever have more trainers on a side than there are battler + # positions on that side. This method doesn't account for if you do. + def pbGetOwnerIndexFromBattlerIndex(idxBattler) + trainer = (opposes?(idxBattler)) ? @opponent : @player + return 0 if !trainer + case trainer.length + when 2 + n = pbSideSize(idxBattler%2) + return [0,0,1][idxBattler/2] if n==3 + return idxBattler/2 # Same as [0,1][idxBattler/2], i.e. 2 battler slots + when 3 + return idxBattler/2 + end + return 0 + end + + def pbGetOwnerFromBattlerIndex(idxBattler) + idxTrainer = pbGetOwnerIndexFromBattlerIndex(idxBattler) + return (opposes?(idxBattler)) ? @opponent[idxTrainer] : @player[idxTrainer] + end + + def pbGetOwnerIndexFromPartyIndex(idxBattler,idxParty) + ret = -1 + pbPartyStarts(idxBattler).each_with_index do |start,i| + break if start>idxParty + ret = i + end + return ret + end + + # Only used for the purpose of an error message when one trainer tries to + # switch another trainer's Pokémon. + def pbGetOwnerFromPartyIndex(idxBattler,idxParty) + idxTrainer = pbGetOwnerIndexFromPartyIndex(idxBattler,idxParty) + return (opposes?(idxBattler)) ? @opponent[idxTrainer] : @player[idxTrainer] + end + + def pbGetOwnerName(idxBattler) + idxTrainer = pbGetOwnerIndexFromBattlerIndex(idxBattler) + return @opponent[idxTrainer].full_name if opposes?(idxBattler) # Opponent + return @player[idxTrainer].full_name if idxTrainer>0 # Ally trainer + return @player[idxTrainer].name # Player + end + + def pbGetOwnerItems(idxBattler) + return [] if !@items || !opposes?(idxBattler) + return @items[pbGetOwnerIndexFromBattlerIndex(idxBattler)] + end + + # Returns whether the battler in position idxBattler is owned by the same + # trainer that owns the Pokémon in party slot idxParty. This assumes that + # both the battler position and the party slot are from the same side. + def pbIsOwner?(idxBattler,idxParty) + idxTrainer1 = pbGetOwnerIndexFromBattlerIndex(idxBattler) + idxTrainer2 = pbGetOwnerIndexFromPartyIndex(idxBattler,idxParty) + return idxTrainer1==idxTrainer2 + end + + def pbOwnedByPlayer?(idxBattler) + return false if opposes?(idxBattler) + return pbGetOwnerIndexFromBattlerIndex(idxBattler)==0 + end + + # Returns the number of Pokémon positions controlled by the given trainerIndex + # on the given side of battle. + def pbNumPositions(side,idxTrainer) + ret = 0 + for i in 0...pbSideSize(side) + t = pbGetOwnerIndexFromBattlerIndex(i*2+side) + next if t!=idxTrainer + ret += 1 + end + return ret + end + + #============================================================================= + # Get party information (counts all teams on the same side) + #============================================================================= + def pbParty(idxBattler) + return (opposes?(idxBattler)) ? @party2 : @party1 + end + + def pbOpposingParty(idxBattler) + return (opposes?(idxBattler)) ? @party1 : @party2 + end + + def pbPartyOrder(idxBattler) + return (opposes?(idxBattler)) ? @party2order : @party1order + end + + def pbPartyStarts(idxBattler) + return (opposes?(idxBattler)) ? @party2starts : @party1starts + end + + # Returns the player's team in its display order. Used when showing the party + # screen. + def pbPlayerDisplayParty(idxBattler=0) + partyOrders = pbPartyOrder(idxBattler) + idxStart, _idxEnd = pbTeamIndexRangeFromBattlerIndex(idxBattler) + ret = [] + eachInTeamFromBattlerIndex(idxBattler) { |pkmn,i| ret[partyOrders[i]-idxStart] = pkmn } + return ret + end + + def pbAbleCount(idxBattler=0) + party = pbParty(idxBattler) + count = 0 + party.each { |pkmn| count += 1 if pkmn && pkmn.able? } + return count + end + + def pbAbleNonActiveCount(idxBattler=0) + party = pbParty(idxBattler) + inBattleIndices = [] + eachSameSideBattler(idxBattler) { |b| inBattleIndices.push(b.pokemonIndex) } + count = 0 + party.each_with_index do |pkmn,idxParty| + next if !pkmn || !pkmn.able? + next if inBattleIndices.include?(idxParty) + count += 1 + end + return count + end + + def pbAllFainted?(idxBattler=0) + return pbAbleCount(idxBattler)==0 + end + + # For the given side of the field (0=player's, 1=opponent's), returns an array + # containing the number of able Pokémon in each team. + def pbAbleTeamCounts(side) + party = pbParty(side) + partyStarts = pbPartyStarts(side) + ret = [] + idxTeam = -1 + nextStart = 0 + party.each_with_index do |pkmn,i| + if i>=nextStart + idxTeam += 1 + nextStart = (idxTeam=idxPartyStart && i=idxPartyStart && i=idxPartyEnd # Check the team only + next if !pkmn || !pkmn.able? # Can't copy a non-fainted Pokémon or egg + ret = i if ret < 0 || partyOrders[i] > partyOrders[ret] + end + return ret + end + + # Used to calculate money gained/lost after winning/losing a battle. + def pbMaxLevelInTeam(side,idxTrainer) + ret = 1 + eachInTeam(side,idxTrainer) do |pkmn,_i| + ret = pkmn.level if pkmn.level>ret + end + return ret + end + + #============================================================================= + # Iterate through battlers + #============================================================================= + def eachBattler + @battlers.each { |b| yield b if b && !b.fainted? } + end + + def eachSameSideBattler(idxBattler=0) + idxBattler = idxBattler.index if idxBattler.respond_to?("index") + @battlers.each { |b| yield b if b && !b.fainted? && !b.opposes?(idxBattler) } + end + + def eachOtherSideBattler(idxBattler=0) + idxBattler = idxBattler.index if idxBattler.respond_to?("index") + @battlers.each { |b| yield b if b && !b.fainted? && b.opposes?(idxBattler) } + end + + def pbSideBattlerCount(idxBattler=0) + ret = 0 + eachSameSideBattler(idxBattler) { |_b| ret += 1 } + return ret + end + + def pbOpposingBattlerCount(idxBattler=0) + ret = 0 + eachOtherSideBattler(idxBattler) { |_b| ret += 1 } + return ret + end + + # This method only counts the player's Pokémon, not a partner trainer's. + def pbPlayerBattlerCount + ret = 0 + eachSameSideBattler { |b| ret += 1 if b.pbOwnedByPlayer? } + return ret + end + + def pbCheckGlobalAbility(abil) + eachBattler { |b| return b if b.hasActiveAbility?(abil) } + return nil + end + + def pbCheckOpposingAbility(abil,idxBattler=0,nearOnly=false) + eachOtherSideBattler(idxBattler) do |b| + next if nearOnly && !b.near?(idxBattler) + return b if b.hasActiveAbility?(abil) + end + return nil + end + + # Given a battler index, and using battle side sizes, returns an array of + # battler indices from the opposing side that are in order of most "opposite". + # Used when choosing a target and pressing up/down to move the cursor to the + # opposite side, and also when deciding which target to select first for some + # moves. + def pbGetOpposingIndicesInOrder(idxBattler) + case pbSideSize(0) + when 1 + case pbSideSize(1) + when 1 # 1v1 single + return [0] if opposes?(idxBattler) + return [1] + when 2 # 1v2 + return [0] if opposes?(idxBattler) + return [3,1] + when 3 # 1v3 + return [0] if opposes?(idxBattler) + return [3,5,1] + end + when 2 + case pbSideSize(1) + when 1 # 2v1 + return [0,2] if opposes?(idxBattler) + return [1] + when 2 # 2v2 double + return [[3,1],[2,0],[1,3],[0,2]][idxBattler] + when 3 # 2v3 + return [[5,3,1],[2,0],[3,1,5]][idxBattler] if idxBattler<3 + return [0,2] + end + when 3 + case pbSideSize(1) + when 1 # 3v1 + return [2,0,4] if opposes?(idxBattler) + return [1] + when 2 # 3v2 + return [[3,1],[2,4,0],[3,1],[2,0,4],[1,3]][idxBattler] + when 3 # 3v3 triple + return [[5,3,1],[4,2,0],[3,5,1],[2,0,4],[1,3,5],[0,2,4]][idxBattler] + end + end + return [idxBattler] + end + + #============================================================================= + # Comparing the positions of two battlers + #============================================================================= + def opposes?(idxBattler1,idxBattler2=0) + idxBattler1 = idxBattler1.index if idxBattler1.respond_to?("index") + idxBattler2 = idxBattler2.index if idxBattler2.respond_to?("index") + return (idxBattler1&1)!=(idxBattler2&1) + end + + def nearBattlers?(idxBattler1,idxBattler2) + return false if idxBattler1==idxBattler2 + return true if pbSideSize(0)<=2 && pbSideSize(1)<=2 + # Get all pairs of battler positions that are not close to each other + pairsArray = [[0,4],[1,5]] # Covers 3v1 and 1v3 + case pbSideSize(0) + when 3 + case pbSideSize(1) + when 3 # 3v3 (triple) + pairsArray.push([0,1]) + pairsArray.push([4,5]) + when 2 # 3v2 + pairsArray.push([0,1]) + pairsArray.push([3,4]) + end + when 2 # 2v3 + pairsArray.push([0,1]) + pairsArray.push([2,5]) + end + # See if any pair matches the two battlers being assessed + pairsArray.each do |pair| + return false if pair.include?(idxBattler1) && pair.include?(idxBattler2) + end + return true + end + + #============================================================================= + # Altering a party or rearranging battlers + #============================================================================= + def pbRemoveFromParty(idxBattler,idxParty) + party = pbParty(idxBattler) + # Erase the Pokémon from the party + party[idxParty] = nil + # Rearrange the display order of the team to place the erased Pokémon last + # in it (to avoid gaps) + partyOrders = pbPartyOrder(idxBattler) + partyStarts = pbPartyStarts(idxBattler) + idxTrainer = pbGetOwnerIndexFromPartyIndex(idxBattler,idxParty) + idxPartyStart = partyStarts[idxTrainer] + idxPartyEnd = (idxTrainer=idxPartyEnd # Only check the team + next if partyOrders[i]0 && user && user.itemActive? + duration = BattleHandlers.triggerWeatherExtenderItem(user.item, + @field.weather,duration,user,self) + end + @field.weatherDuration = duration + weather_data = GameData::BattleWeather.try_get(@field.weather) + pbCommonAnimation(weather_data.animation) if showAnim && weather_data + pbHideAbilitySplash(user) if user + case @field.weather + when :Sun then pbDisplay(_INTL("The sunlight turned harsh!")) + when :Rain then pbDisplay(_INTL("It started to rain!")) + when :Sandstorm then pbDisplay(_INTL("A sandstorm brewed!")) + when :Hail then pbDisplay(_INTL("It started to hail!")) + when :HarshSun then pbDisplay(_INTL("The sunlight turned extremely harsh!")) + when :HeavyRain then pbDisplay(_INTL("A heavy rain began to fall!")) + when :StrongWinds then pbDisplay(_INTL("Mysterious strong winds are protecting Flying-type Pokémon!")) + when :ShadowSky then pbDisplay(_INTL("A shadow sky appeared!")) + end + # Check for end of primordial weather, and weather-triggered form changes + eachBattler { |b| b.pbCheckFormOnWeatherChange } + pbEndPrimordialWeather + end + + def pbEndPrimordialWeather + oldWeather = @field.weather + # End Primordial Sea, Desolate Land, Delta Stream + case @field.weather + when :HarshSun + if !pbCheckGlobalAbility(:DESOLATELAND) + @field.weather = :None + pbDisplay("The harsh sunlight faded!") + end + when :HeavyRain + if !pbCheckGlobalAbility(:PRIMORDIALSEA) + @field.weather = :None + pbDisplay("The heavy rain has lifted!") + end + # when :StrongWinds + # if !pbCheckGlobalAbility(:DELTASTREAM) + # @field.weather = :None + # pbDisplay("The mysterious air current has dissipated!") + # end + end + if @field.weather!=oldWeather + # Check for form changes caused by the weather changing + eachBattler { |b| b.pbCheckFormOnWeatherChange } + # Start up the default weather + pbStartWeather(nil,@field.defaultWeather) if @field.defaultWeather != :None + end + end + + def defaultTerrain=(value) + @field.defaultTerrain = value + @field.terrain = value + @field.terrainDuration = -1 + end + + def pbStartTerrain(user,newTerrain,fixedDuration=true) + return if @field.terrain==newTerrain + @field.terrain = newTerrain + duration = (fixedDuration) ? 5 : -1 + if duration>0 && user && user.itemActive? + duration = BattleHandlers.triggerTerrainExtenderItem(user.item, + newTerrain,duration,user,self) + end + @field.terrainDuration = duration + terrain_data = GameData::BattleTerrain.try_get(@field.terrain) + pbCommonAnimation(terrain_data.animation) if terrain_data + pbHideAbilitySplash(user) if user + case @field.terrain + when :Electric + pbDisplay(_INTL("An electric current runs across the battlefield!")) + when :Grassy + pbDisplay(_INTL("Grass grew to cover the battlefield!")) + when :Misty + pbDisplay(_INTL("Mist swirled about the battlefield!")) + when :Psychic + pbDisplay(_INTL("The battlefield got weird!")) + end + # Check for terrain seeds that boost stats in a terrain + eachBattler { |b| b.pbItemTerrainStatBoostCheck } + end + + #============================================================================= + # Messages and animations + #============================================================================= + def pbDisplay(msg,&block) + @scene.pbDisplayMessage(msg,&block) + end + + def pbDisplayBrief(msg) + @scene.pbDisplayMessage(msg,true) + end + + def pbDisplayPaused(msg,&block) + @scene.pbDisplayPausedMessage(msg,&block) + end + + def pbDisplayConfirm(msg) + return @scene.pbDisplayConfirmMessage(msg) + end + + def pbShowCommands(msg,commands,canCancel=true) + @scene.pbShowCommands(msg,commands,canCancel) + end + + def pbAnimation(move,user,targets,hitNum=0) + @scene.pbAnimation(move,user,targets,hitNum) if @showAnims + end + + def pbCommonAnimation(name,user=nil,targets=nil) + @scene.pbCommonAnimation(name,user,targets) if @showAnims + end + + + + def pbShowAbilitySplash(battler,delay=false,logTrigger=true,abilityName=nil) + PBDebug.log("[Ability triggered] #{battler.pbThis}'s #{battler.abilityName}") if logTrigger + return if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @scene.pbShowAbilitySplash(battler,false ,abilityName) + if delay + Graphics.frame_rate.times { @scene.pbUpdate } # 1 second + end + end + + def pbHideAbilitySplash(battler) + return if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @scene.pbHideAbilitySplash(battler) + end + + def pbReplaceAbilitySplash(battler) + return if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + @scene.pbReplaceAbilitySplash(battler) + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/003_Battle_StartAndEnd.rb b/Data/Scripts/011_Battle/003_Battle/003_Battle_StartAndEnd.rb new file mode 100644 index 000000000..ee7766124 --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/003_Battle_StartAndEnd.rb @@ -0,0 +1,581 @@ +class PokeBattle_Battle + class BattleAbortedException < Exception; end + + def pbAbort + raise BattleAbortedException.new("Battle aborted") + end + + #============================================================================= + # Makes sure all Pokémon exist that need to. Alter the type of battle if + # Makes sure all Pokémon exist that need to. Alter the type of battle if + # necessary. Will never try to create battler positions, only delete them + # (except for wild Pokémon whose number of positions are fixed). Reduces the + # size of each side by 1 and tries again. If the side sizes are uneven, only + # the larger side's size will be reduced by 1 each time, until both sides are + # an equal size (then both sides will be reduced equally). + #============================================================================= + def pbEnsureParticipants + # Prevent battles larger than 2v2 if both sides have multiple trainers + # NOTE: This is necessary to ensure that battlers can never become unable to + # hit each other due to being too far away. In such situations, + # battlers will move to the centre position at the end of a round, but + # because they cannot move into a position owned by a different + # trainer, it's possible that battlers will be unable to move close + # enough to hit each other if there are multiple trainers on each + # side. + if trainerBattle? && (@sideSizes[0]>2 || @sideSizes[1]>2) && + @player.length>1 && @opponent.length>1 + raise _INTL("Can't have battles larger than 2v2 where both sides have multiple trainers") + end + # Find out how many Pokémon each trainer has + side1counts = pbAbleTeamCounts(0) + side2counts = pbAbleTeamCounts(1) + # Change the size of the battle depending on how many wild Pokémon there are + if wildBattle? && side2counts[0]!=@sideSizes[1] + if @sideSizes[0]==@sideSizes[1] + # Even number of battlers per side, change both equally + @sideSizes = [side2counts[0],side2counts[0]] + else + # Uneven number of battlers per side, just change wild side's size + @sideSizes[1] = side2counts[0] + end + end + # Check if battle is possible, including changing the number of battlers per + # side if necessary + loop do + needsChanging = false + for side in 0...2 # Each side in turn + next if side==1 && wildBattle? # Wild side's size already checked above + sideCounts = (side==0) ? side1counts : side2counts + requireds = [] + # Find out how many Pokémon each trainer on side needs to have + for i in 0...@sideSizes[side] + idxTrainer = pbGetOwnerIndexFromBattlerIndex(i*2+side) + requireds[idxTrainer] = 0 if requireds[idxTrainer].nil? + requireds[idxTrainer] += 1 + end + # Compare the have values with the need values + if requireds.length>sideCounts.length + raise _INTL("Error: def pbGetOwnerIndexFromBattlerIndex gives invalid owner index ({1} for battle type {2}v{3}, trainers {4}v{5})", + requireds.length-1,@sideSizes[0],@sideSizes[1],side1counts.length,side2counts.length) + end + sideCounts.each_with_index do |_count,i| + if !requireds[i] || requireds[i]==0 + raise _INTL("Player-side trainer {1} has no battler position for their Pokémon to go (trying {2}v{3} battle)", + i+1,@sideSizes[0],@sideSizes[1]) if side==0 + raise _INTL("Opposing trainer {1} has no battler position for their Pokémon to go (trying {2}v{3} battle)", + i+1,@sideSizes[0],@sideSizes[1]) if side==1 + end + next if requireds[i]<=sideCounts[i] # Trainer has enough Pokémon to fill their positions + if requireds[i]==1 + raise _INTL("Player-side trainer {1} has no able Pokémon",i+1) if side==0 + raise _INTL("Opposing trainer {1} has no able Pokémon",i+1) if side==1 + end + # Not enough Pokémon, try lowering the number of battler positions + needsChanging = true + break + end + break if needsChanging + end + break if !needsChanging + # Reduce one or both side's sizes by 1 and try again + if wildBattle? + PBDebug.log("#{@sideSizes[0]}v#{@sideSizes[1]} battle isn't possible " + + "(#{side1counts} player-side teams versus #{side2counts[0]} wild Pokémon)") + newSize = @sideSizes[0]-1 + else + PBDebug.log("#{@sideSizes[0]}v#{@sideSizes[1]} battle isn't possible " + + "(#{side1counts} player-side teams versus #{side2counts} opposing teams)") + newSize = @sideSizes.max-1 + end + if newSize==0 + raise _INTL("Couldn't lower either side's size any further, battle isn't possible") + end + for side in 0...2 + next if side==1 && wildBattle? # Wild Pokémon's side size is fixed + next if @sideSizes[side]==1 || newSize>@sideSizes[side] + @sideSizes[side] = newSize + end + PBDebug.log("Trying #{@sideSizes[0]}v#{@sideSizes[1]} battle instead") + end + end + + #============================================================================= + # Set up all battlers + #============================================================================= + def pbCreateBattler(idxBattler,pkmn,idxParty) + if !@battlers[idxBattler].nil? + raise _INTL("Battler index {1} already exists",idxBattler) + end + @battlers[idxBattler] = PokeBattle_Battler.new(self,idxBattler) + @positions[idxBattler] = PokeBattle_ActivePosition.new + pbClearChoice(idxBattler) + @successStates[idxBattler] = PokeBattle_SuccessState.new + @battlers[idxBattler].pbInitialize(pkmn,idxParty) + end + + def pbSetUpSides + ret = [[],[]] + for side in 0...2 + # Set up wild Pokémon + if side==1 && wildBattle? + pbParty(1).each_with_index do |pkmn,idxPkmn| + pbCreateBattler(2*idxPkmn+side,pkmn,idxPkmn) + # Changes the Pokémon's form upon entering battle (if it should) + @peer.pbOnEnteringBattle(self,pkmn,true) + pbSetSeen(@battlers[2*idxPkmn+side]) + @usedInBattle[side][idxPkmn] = true + end + next + end + # Set up player's Pokémon and trainers' Pokémon + trainer = (side==0) ? @player : @opponent + requireds = [] + # Find out how many Pokémon each trainer on side needs to have + for i in 0...@sideSizes[side] + idxTrainer = pbGetOwnerIndexFromBattlerIndex(i*2+side) + requireds[idxTrainer] = 0 if requireds[idxTrainer].nil? + requireds[idxTrainer] += 1 + end + # For each trainer in turn, find the needed number of Pokémon for them to + # send out, and initialize them + battlerNumber = 0 + trainer.each_with_index do |_t,idxTrainer| + ret[side][idxTrainer] = [] + eachInTeam(side,idxTrainer) do |pkmn,idxPkmn| + next if !pkmn.able? + idxBattler = 2*battlerNumber+side + pbCreateBattler(idxBattler,pkmn,idxPkmn) + ret[side][idxTrainer].push(idxBattler) + battlerNumber += 1 + break if ret[side][idxTrainer].length>=requireds[idxTrainer] + end + end + end + return ret + end + + #============================================================================= + # Send out all battlers at the start of battle + #============================================================================= + def pbStartBattleSendOut(sendOuts) + # "Want to battle" messages + if wildBattle? + foeParty = pbParty(1) + case foeParty.length + when 1 + pbDisplayPaused(_INTL("Oh! A wild {1} appeared!",foeParty[0].name)) + when 2 + pbDisplayPaused(_INTL("Oh! A wild {1} and {2} appeared!",foeParty[0].name, + foeParty[1].name)) + when 3 + pbDisplayPaused(_INTL("Oh! A wild {1}, {2} and {3} appeared!",foeParty[0].name, + foeParty[1].name,foeParty[2].name)) + end + else # Trainer battle + case @opponent.length + when 1 + pbDisplayPaused(_INTL("You are challenged by {1}!",@opponent[0].full_name)) + when 2 + pbDisplayPaused(_INTL("You are challenged by {1} and {2}!",@opponent[0].full_name, + @opponent[1].full_name)) + when 3 + pbDisplayPaused(_INTL("You are challenged by {1}, {2} and {3}!", + @opponent[0].full_name,@opponent[1].full_name,@opponent[2].full_name)) + end + end + # Send out Pokémon (opposing trainers first) + for side in [1,0] + next if side==1 && wildBattle? + msg = "" + toSendOut = [] + trainers = (side==0) ? @player : @opponent + # Opposing trainers and partner trainers's messages about sending out Pokémon + trainers.each_with_index do |t,i| + next if side==0 && i==0 # The player's message is shown last + msg += "\r\n" if msg.length>0 + sent = sendOuts[side][i] + case sent.length + when 1 + msg += _INTL("{1} sent out {2}!",t.full_name,@battlers[sent[0]].name) + when 2 + msg += _INTL("{1} sent out {2} and {3}!",t.full_name, + @battlers[sent[0]].name,@battlers[sent[1]].name) + when 3 + if $game_switches[SWITCH_TRIPLE_BOSS_BATTLE] + if $game_switches[SWITCH_SILVERBOSS_BATTLE] + msg += _INTL("A wild Paldiatina appeared!",t.full_name) + else + msg += _INTL("{1} sent out Zapmolcuno!",t.full_name) + end + else + msg += _INTL("{1} sent out {2}, {3} and {4}!",t.full_name, + @battlers[sent[0]].name,@battlers[sent[1]].name,@battlers[sent[2]].name) + end + + end + toSendOut.concat(sent) + end + # The player's message about sending out Pokémon + if side==0 + msg += "\r\n" if msg.length>0 + sent = sendOuts[side][0] + case sent.length + when 1 + msg += _INTL("Go! {1}!",@battlers[sent[0]].name) + when 2 + msg += _INTL("Go! {1} and {2}!",@battlers[sent[0]].name,@battlers[sent[1]].name) + when 3 + msg += _INTL("Go! {1}, {2} and {3}!",@battlers[sent[0]].name, + @battlers[sent[1]].name,@battlers[sent[2]].name) + end + toSendOut.concat(sent) + end + pbDisplayBrief(msg) if msg.length>0 + # The actual sending out of Pokémon + animSendOuts = [] + toSendOut.each do |idxBattler| + animSendOuts.push([idxBattler,@battlers[idxBattler].pokemon]) + end + pbSendOut(animSendOuts,true) + end + end + + #============================================================================= + # Start a battle + #============================================================================= + def pbStartBattle + PBDebug.log("") + PBDebug.log("******************************************") + logMsg = "[Started battle] " + if @sideSizes[0]==1 && @sideSizes[1]==1 + logMsg += "Single " + elsif @sideSizes[0]==2 && @sideSizes[1]==2 + logMsg += "Double " + elsif @sideSizes[0]==3 && @sideSizes[1]==3 + logMsg += "Triple " + else + logMsg += "#{@sideSizes[0]}v#{@sideSizes[1]} " + end + logMsg += "wild " if wildBattle? + logMsg += "trainer " if trainerBattle? + logMsg += "battle (#{@player.length} trainer(s) vs. " + logMsg += "#{pbParty(1).length} wild Pokémon)" if wildBattle? + logMsg += "#{@opponent.length} trainer(s))" if trainerBattle? + PBDebug.log(logMsg) + pbEnsureParticipants + begin + pbStartBattleCore + rescue BattleAbortedException + @decision = 0 + @scene.pbEndBattle(@decision) + end + return @decision + end + + def pbStartBattleCore + # Set up the battlers on each side + sendOuts = pbSetUpSides + # Create all the sprites and play the battle intro animation + @scene.pbStartBattle(self) + # Show trainers on both sides sending out Pokémon + pbStartBattleSendOut(sendOuts) + # Weather announcement + weather_data = GameData::BattleWeather.try_get(@field.weather) + echoln "Current weather: #{@field.weather}" + + pbCommonAnimation(weather_data.animation) if weather_data + case @field.weather + when :Sun then pbDisplay(_INTL("The sunlight is strong.")) + when :Rain then pbDisplay(_INTL("It is raining.")) + when :Sandstorm then pbDisplay(_INTL("A sandstorm is raging.")) + when :Hail then pbDisplay(_INTL("Hail is falling.")) + when :HarshSun then pbDisplay(_INTL("The sunlight is extremely harsh.")) + when :HeavyRain then pbDisplay(_INTL("It is raining heavily.")) + when :StrongWinds then pbDisplay(_INTL("The wind is strong.")) + when :ShadowSky then pbDisplay(_INTL("The sky is shadowy.")) + end + # Terrain announcement + terrain_data = GameData::BattleTerrain.try_get(@field.terrain) + pbCommonAnimation(terrain_data.animation) if terrain_data + case @field.terrain + when :Electric + pbDisplay(_INTL("An electric current runs across the battlefield!")) + when :Grassy + pbDisplay(_INTL("Grass is covering the battlefield!")) + when :Misty + pbDisplay(_INTL("Mist swirls about the battlefield!")) + when :Psychic + pbDisplay(_INTL("The battlefield is weird!")) + end + # Abilities upon entering battle + pbOnActiveAll + # Main battle loop + pbBattleLoop + end + + #============================================================================= + # Main battle loop + #============================================================================= + def pbBattleLoop + @turnCount = 0 + loop do # Now begin the battle loop + PBDebug.log("") + PBDebug.log("***Round #{@turnCount+1}***") + if @debug && @turnCount>=100 + @decision = pbDecisionOnTime + PBDebug.log("") + PBDebug.log("***Undecided after 100 rounds, aborting***") + pbAbort + break + end + PBDebug.log("") + # Command phase + PBDebug.logonerr { pbCommandPhase } + break if @decision>0 + # Attack phase + PBDebug.logonerr { pbAttackPhase } + break if @decision>0 + # End of round phase + PBDebug.logonerr { pbEndOfRoundPhase } + break if @decision>0 + @turnCount += 1 + end + pbEndOfBattle + end + + #============================================================================= + # End of battle + #============================================================================= + def pbGainMoney + return if $game_switches[SWITCH_IS_REMATCH] #is rematch + return if !@internalBattle || !@moneyGain + # Money rewarded from opposing trainers + if trainerBattle? + tMoney = 0 + @opponent.each_with_index do |t,i| + tMoney += pbMaxLevelInTeam(1, i) * t.base_money + end + tMoney *= 2 if @field.effects[PBEffects::AmuletCoin] + tMoney *= 2 if @field.effects[PBEffects::HappyHour] + oldMoney = pbPlayer.money + pbPlayer.money += tMoney + moneyGained = pbPlayer.money-oldMoney + if moneyGained>0 + pbDisplayPaused(_INTL("You got ${1} for winning!",moneyGained.to_s_formatted)) + end + end + # Pick up money scattered by Pay Day + if @field.effects[PBEffects::PayDay]>0 + @field.effects[PBEffects::PayDay] *= 2 if @field.effects[PBEffects::AmuletCoin] + @field.effects[PBEffects::PayDay] *= 2 if @field.effects[PBEffects::HappyHour] + oldMoney = pbPlayer.money + pbPlayer.money += @field.effects[PBEffects::PayDay] + moneyGained = pbPlayer.money-oldMoney + if moneyGained>0 + pbDisplayPaused(_INTL("You picked up ${1}!",moneyGained.to_s_formatted)) + end + end + end + + def pbLoseMoney + return if !@internalBattle || !@moneyGain + return if $game_switches[Settings::NO_MONEY_LOSS] + maxLevel = pbMaxLevelInTeam(0,0) # Player's Pokémon only, not partner's + multiplier = [8,16,24,36,48,64,80,100,120] + idxMultiplier = [pbPlayer.badge_count, multiplier.length - 1].min + tMoney = maxLevel*multiplier[idxMultiplier] + tMoney = pbPlayer.money if tMoney>pbPlayer.money + oldMoney = pbPlayer.money + pbPlayer.money -= tMoney + moneyLost = oldMoney-pbPlayer.money + if moneyLost>0 + if trainerBattle? + pbDisplayPaused(_INTL("You gave ${1} to the winner...",moneyLost.to_s_formatted)) + else + pbDisplayPaused(_INTL("You panicked and dropped ${1}...",moneyLost.to_s_formatted)) + end + end + end + + def pbEndOfBattle + oldDecision = @decision + @decision = 4 if @decision==1 && wildBattle? && @caughtPokemon.length>0 + case oldDecision + ##### WIN ##### + when 1 + PBDebug.log("") + PBDebug.log("***Player won***") + if trainerBattle? + @scene.pbTrainerBattleSuccess + case @opponent.length + when 1 + pbDisplayPaused(_INTL("You defeated {1}!",@opponent[0].full_name)) + when 2 + pbDisplayPaused(_INTL("You defeated {1} and {2}!",@opponent[0].full_name, + @opponent[1].full_name)) + when 3 + pbDisplayPaused(_INTL("You defeated {1}, {2} and {3}!",@opponent[0].full_name, + @opponent[1].full_name,@opponent[2].full_name)) + end + @opponent.each_with_index do |_t,i| + @scene.pbShowOpponent(i) + msg = (@endSpeeches[i] && @endSpeeches[i] !="") ? @endSpeeches[i] : "..." + pbDisplayPaused(msg.gsub(/\\[Pp][Nn]/,pbPlayer.name)) + end + end + # Gain money from winning a trainer battle, and from Pay Day + pbGainMoney if @decision!=4 + # Hide remaining trainer + @scene.pbShowOpponent(@opponent.length) if trainerBattle? && @caughtPokemon.length>0 + if $game_switches[AUTOSAVE_WIN_SWITCH] + Kernel.tryAutosave() + end + + ##### LOSE, DRAW ##### + when 2, 5 + PBDebug.log("") + PBDebug.log("***Player lost***") if @decision==2 + PBDebug.log("***Player drew with opponent***") if @decision==5 + if @internalBattle + pbDisplayPaused(_INTL("You have no more Pokémon that can fight!")) + if trainerBattle? + case @opponent.length + when 1 + pbDisplayPaused(_INTL("You lost against {1}!",@opponent[0].full_name)) + when 2 + pbDisplayPaused(_INTL("You lost against {1} and {2}!", + @opponent[0].full_name,@opponent[1].full_name)) + when 3 + pbDisplayPaused(_INTL("You lost against {1}, {2} and {3}!", + @opponent[0].full_name,@opponent[1].full_name,@opponent[2].full_name)) + end + end + # Lose money from losing a battle + pbLoseMoney + pbDisplayPaused(_INTL("You blacked out!")) if !@canLose + elsif @decision==2 + if @opponent + @opponent.each_with_index do |_t,i| + @scene.pbShowOpponent(i) + msg = (@endSpeechesWin[i] && @endSpeechesWin[i]!="") ? @endSpeechesWin[i] : "..." + pbDisplayPaused(msg.gsub(/\\[Pp][Nn]/,pbPlayer.name)) + end + end + end + ##### CAUGHT WILD POKÉMON ##### + when 4 + @scene.pbWildBattleSuccess if !Settings::GAIN_EXP_FOR_CAPTURE + end + # Register captured Pokémon in the Pokédex, and store them + #pbRecordAndStoreCaughtPokemon + + isRematch = $game_switches[SWITCH_IS_REMATCH] + begin + if isRematch + if @opponent.is_a?(Array) + for trainer in @opponent + rematchId = getRematchId(trainer.name,trainer.trainer_type) + incrNbRematches(rematchId) + end + else + rematchId = getRematchId(@opponent.name,@opponent.trainer_type) + incrNbRematches(rematchId) + end + end + rescue + $game_switches[SWITCH_IS_REMATCH]=false + end + + + # Collect Pay Day money in a wild battle that ended in a capture + pbGainMoney if @decision==4 + # Pass on Pokérus within the party + if @internalBattle + infected = [] + $Trainer.party.each_with_index do |pkmn,i| + infected.push(i) if pkmn.pokerusStage==1 + end + infected.each do |idxParty| + strain = $Trainer.party[idxParty].pokerusStrain + if idxParty>0 && $Trainer.party[idxParty-1].pokerusStage==0 + $Trainer.party[idxParty-1].givePokerus(strain) if rand(3)==0 # 33% + end + if idxParty<$Trainer.party.length-1 && $Trainer.party[idxParty+1].pokerusStage==0 + $Trainer.party[idxParty+1].givePokerus(strain) if rand(3)==0 # 33% + end + end + end + + pbParty(0).each_with_index do |pkmn,i| + next if !pkmn + @peer.pbOnLeavingBattle(self,pkmn,@usedInBattle[0][i],true) # Reset form + pkmn.item = @initialItems[0][i] + pkmn.spriteform_head=nil + pkmn.spriteform_body=nil + end + pbRecordAndStoreCaughtPokemon + + # Clean up battle stuff + @scene.pbEndBattle(@decision) + @battlers.each do |b| + next if !b + pbCancelChoice(b.index) # Restore unused items to Bag + BattleHandlers.triggerAbilityOnSwitchOut(b.ability,b,true) if b.abilityActive? + end + + return @decision + end + + #============================================================================= + # Judging + #============================================================================= + def pbJudgeCheckpoint(user,move=nil); end + + def pbDecisionOnTime + counts = [0,0] + hpTotals = [0,0] + for side in 0...2 + pbParty(side).each do |pkmn| + next if !pkmn || !pkmn.able? + counts[side] += 1 + hpTotals[side] += pkmn.hp + end + end + return 1 if counts[0]>counts[1] # Win (player has more able Pokémon) + return 2 if counts[0]hpTotals[1] # Win (player has more HP in total) + return 2 if hpTotals[0]1 + end + return 1 if counts[0]>counts[1] # Win (player has more able Pokémon) + return 2 if counts[0]hpTotals[1] # Win (player has a bigger average HP %) + return 2 if hpTotals[0] 0 || expShare.length > 0 || expAll + # Gain EVs and Exp for participants + eachInTeam(0, 0) do |pkmn, i| + next if !pkmn.able? + next unless b.participants.include?(i) || expShare.include?(i) + pbGainEVsOne(i, b) + pbGainExpOne(i, b, numPartic, expShare, expAll) + end + # Gain EVs and Exp for all other Pokémon because of Exp All + if expAll + showMessage = true + eachInTeam(0, 0) do |pkmn, i| + next if !pkmn.able? + next if b.participants.include?(i) || expShare.include?(i) + pbDisplayPaused(_INTL("Your party Pokémon in waiting also got Exp. Points!")) if showMessage + showMessage = false + pbGainEVsOne(i, b) + pbGainExpOne(i, b, numPartic, expShare, expAll, false) + end + end + end + # Clear the participants array + b.participants = [] + end + end + + def pbGainEVsOne(idxParty, defeatedBattler) + pkmn = pbParty(0)[idxParty] # The Pokémon gaining EVs from defeatedBattler + evYield = defeatedBattler.pokemon.evYield + # Num of effort points pkmn already has + evTotal = 0 + GameData::Stat.each_main { |s| evTotal += pkmn.ev[s.id] } + # Modify EV yield based on pkmn's held item + if !BattleHandlers.triggerEVGainModifierItem(pkmn.item, pkmn, evYield) + BattleHandlers.triggerEVGainModifierItem(@initialItems[0][idxParty], pkmn, evYield) + end + # Double EV gain because of Pokérus + if pkmn.pokerusStage >= 1 # Infected or cured + evYield.each_key { |stat| evYield[stat] *= 2 } + end + # Gain EVs for each stat in turn + if pkmn.shadowPokemon? && pkmn.saved_ev + pkmn.saved_ev.each_value { |e| evTotal += e } + GameData::Stat.each_main do |s| + evGain = evYield[s.id].clamp(0, Pokemon::EV_STAT_LIMIT - pkmn.ev[s.id] - pkmn.saved_ev[s.id]) + evGain = evGain.clamp(0, Pokemon::EV_LIMIT - evTotal) + pkmn.saved_ev[s.id] += evGain + evTotal += evGain + end + else + GameData::Stat.each_main do |s| + evGain = evYield[s.id].clamp(0, Pokemon::EV_STAT_LIMIT - pkmn.ev[s.id]) + evGain = evGain.clamp(0, Pokemon::EV_LIMIT - evTotal) + pkmn.ev[s.id] += evGain + evTotal += evGain + end + end + end + + + def pbGainExpOne(idxParty, defeatedBattler, numPartic, expShare, expAll, showMessages = true) + pkmn = pbParty(0)[idxParty] # The Pokémon gaining EVs from defeatedBattler + growth_rate = pkmn.growth_rate + # Don't bother calculating if gainer is already at max Exp + if pkmn.exp >= growth_rate.maximum_exp + pkmn.calc_stats # To ensure new EVs still have an effect + return + end + isPartic = defeatedBattler.participants.include?(idxParty) + hasExpShare = expShare.include?(idxParty) + level = defeatedBattler.level + # Main Exp calculation + exp = 0 + a = level * defeatedBattler.pokemon.base_exp + if expShare.length > 0 && (isPartic || hasExpShare) + if numPartic == 0 # No participants, all Exp goes to Exp Share holders + exp = a / (Settings::SPLIT_EXP_BETWEEN_GAINERS ? expShare.length : 1) + elsif Settings::SPLIT_EXP_BETWEEN_GAINERS # Gain from participating and/or Exp Share + exp = a / (2 * numPartic) if isPartic + exp += a / (2 * expShare.length) if hasExpShare + else + # Gain from participating and/or Exp Share (Exp not split) + exp = (isPartic) ? a : a / 2 + end + elsif isPartic # Participated in battle, no Exp Shares held by anyone + exp = a / (Settings::SPLIT_EXP_BETWEEN_GAINERS ? numPartic : 1) + elsif expAll # Didn't participate in battle, gaining Exp due to Exp All + # NOTE: Exp All works like the Exp Share from Gen 6+, not like the Exp All + # from Gen 1, i.e. Exp isn't split between all Pokémon gaining it. + exp = a / 2 + end + return if exp <= 0 + # Pokémon gain more Exp from trainer battles + exp = (exp * 1.5).floor if trainerBattle? + # Scale the gained Exp based on the gainer's level (or not) + if Settings::SCALED_EXP_FORMULA + exp /= 5 + levelAdjust = (2 * level + 10.0) / (pkmn.level + level + 10.0) + levelAdjust = levelAdjust ** 5 + levelAdjust = Math.sqrt(levelAdjust) + exp *= levelAdjust + exp = exp.floor + exp += 1 if isPartic || hasExpShare + else + exp /= 7 + end + # Foreign Pokémon gain more Exp + isOutsider = (pkmn.owner.id != pbPlayer.id || + (pkmn.owner.language != 0 && pkmn.owner.language != pbPlayer.language)) || + pkmn.isSelfFusion? #also self fusions + if isOutsider + if pkmn.owner.language != 0 && pkmn.owner.language != pbPlayer.language + exp = (exp * 1.7).floor + else + exp = (exp * 1.5).floor + end + end + # Modify Exp gain based on pkmn's held item + i = BattleHandlers.triggerExpGainModifierItem(pkmn.item, pkmn, exp) + if i < 0 + i = BattleHandlers.triggerExpGainModifierItem(@initialItems[0][idxParty], pkmn, exp) + end + exp = i if i >= 0 + # Make sure Exp doesn't exceed the maximum + + exp = 0 if $PokemonSystem.level_caps==1 && pokemonExceedsLevelCap(pkmn) + + expFinal = growth_rate.add_exp(pkmn.exp, exp) + expGained = expFinal - pkmn.exp + + + + + return if expGained <= 0 + # "Exp gained" message + if showMessages + if isOutsider + pbDisplayPaused(_INTL("{1} got a boosted {2} Exp. Points!", pkmn.name, expGained)) + else + pbDisplayPaused(_INTL("{1} got {2} Exp. Points!", pkmn.name, expGained)) + end + end + curLevel = pkmn.level + newLevel = growth_rate.level_from_exp(expFinal) + dontAnimate=false + if newLevel < curLevel + dontAnimate = true + # debugInfo = "Levels: #{curLevel}->#{newLevel} | Exp: #{pkmn.exp}->#{expFinal} | gain: #{expGained}" + # raise RuntimeError.new( + # echoln _INTL("{1}'s new level is less than its\r\ncurrent level, which shouldn't happen.\r\n[Debug: {2}]", + # pkmn.name, debugInfo) + pbDisplayPaused(_INTL("{1}'s growth rate has changed to '{2}''. Its level will be adjusted to reflect its current exp.", pkmn.name, pkmn.growth_rate.real_name)) + end + # Give Exp + if pkmn.shadowPokemon? + pkmn.exp += expGained + return + end + tempExp1 = pkmn.exp + battler = pbFindBattler(idxParty) + loop do + # For each level gained in turn... + # EXP Bar animation + levelMinExp = growth_rate.minimum_exp_for_level(curLevel) + levelMaxExp = growth_rate.minimum_exp_for_level(curLevel + 1) + tempExp2 = (levelMaxExp < expFinal) ? levelMaxExp : expFinal + pkmn.exp = tempExp2 + + + + if pkmn.isFusion? + if pkmn.exp_gained_since_fused == nil + pkmn.exp_gained_since_fused = expGained + else + pkmn.exp_gained_since_fused += expGained + end + + end + @scene.pbEXPBar(battler, levelMinExp, levelMaxExp, tempExp1, tempExp2) if !dontAnimate + + + tempExp1 = tempExp2 + curLevel += 1 + if curLevel > newLevel + # Gained all the Exp now, end the animation + pkmn.calc_stats + battler.pbUpdate(false) if battler + @scene.pbRefreshOne(battler.index) if battler + break + end + # Levelled up + pbCommonAnimation("LevelUp", battler) if battler + oldTotalHP = pkmn.totalhp + oldAttack = pkmn.attack + oldDefense = pkmn.defense + oldSpAtk = pkmn.spatk + oldSpDef = pkmn.spdef + oldSpeed = pkmn.speed + if battler && battler.pokemon + battler.pokemon.changeHappiness("levelup") + end + pkmn.calc_stats + battler.pbUpdate(false) if battler + @scene.pbRefreshOne(battler.index) if battler + pbDisplayPaused(_INTL("{1} grew to Lv. {2}!", pkmn.name, curLevel)) + if !$game_switches[SWITCH_NO_LEVELS_MODE] + @scene.pbLevelUp(pkmn, battler, oldTotalHP, oldAttack, oldDefense, + oldSpAtk, oldSpDef, oldSpeed) + end + + echoln "256" + + # Learn all moves learned at this level + moveList = pkmn.getMoveList + moveList.each { |m| pbLearnMove(idxParty, m[1]) if m[0] == curLevel } + end + end + + #============================================================================= + # Learning a move + #============================================================================= + def pbLearnMove(idxParty, newMove) + pkmn = pbParty(0)[idxParty] + return if !pkmn + pkmnName = pkmn.name + battler = pbFindBattler(idxParty) + moveName = GameData::Move.get(newMove).name + # Pokémon already knows the move + return if pkmn.moves.any? { |m| m && m.id == newMove } + # Pokémon has space for the new move; just learn it + if pkmn.moves.length < Pokemon::MAX_MOVES + move = Pokemon::Move.new(newMove) + pkmn.moves.push(move) + pkmn.add_learned_move(move) + pbDisplay(_INTL("{1} learned {2}!", pkmnName, moveName)) { pbSEPlay("Pkmn move learnt") } + if battler + battler.moves.push(PokeBattle_Move.from_pokemon_move(self, pkmn.moves.last)) + battler.pbCheckFormOnMovesetChange + end + return + end + # Pokémon already knows the maximum number of moves; try to forget one to learn the new move + loop do + pbDisplayPaused(_INTL("{1} wants to learn {2}, but it already knows {3} moves.", + pkmnName, moveName, pkmn.moves.length.to_word)) + if pbDisplayConfirm(_INTL("Forget a move to learn {1}?", moveName)) + pbDisplayPaused(_INTL("Which move should be forgotten?")) + forgetMove = @scene.pbForgetMove(pkmn, newMove) + if forgetMove >= 0 + oldMoveName = pkmn.moves[forgetMove].name + pkmn.moves[forgetMove] = Pokemon::Move.new(newMove) # Replaces current/total PP + pkmn.add_learned_move(newMove) + battler.moves[forgetMove] = PokeBattle_Move.from_pokemon_move(self, pkmn.moves[forgetMove]) if battler + + pbDisplayPaused(_INTL("1, 2, and... ... ... Ta-da!")) + pbDisplayPaused(_INTL("{1} forgot how to use {2}. And...", pkmnName, oldMoveName)) + pbDisplay(_INTL("{1} learned {2}!", pkmnName, moveName)) { pbSEPlay("Pkmn move learnt") } + battler.pbCheckFormOnMovesetChange if battler + break + elsif pbDisplayConfirm(_INTL("Give up on learning {1}?", moveName)) + pbDisplay(_INTL("{1} did not learn {2}.", pkmnName, moveName)) + break + end + elsif pbDisplayConfirm(_INTL("Give up on learning {1}?", moveName)) + pbDisplay(_INTL("{1} did not learn {2}.", pkmnName, moveName)) + break + end + end + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/005_Battle_Action_AttacksPriority.rb b/Data/Scripts/011_Battle/003_Battle/005_Battle_Action_AttacksPriority.rb new file mode 100644 index 000000000..9487bf0af --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/005_Battle_Action_AttacksPriority.rb @@ -0,0 +1,248 @@ +class PokeBattle_Battle + #============================================================================= + # Choosing a move/target + #============================================================================= + def pbCanChooseMove?(idxBattler,idxMove,showMessages,sleepTalk=false) + battler = @battlers[idxBattler] + move = battler.moves[idxMove] + return false unless move + if move.pp==0 && move.total_pp>0 && !sleepTalk + pbDisplayPaused(_INTL("There's no PP left for this move!")) if showMessages + return false + end + if battler.effects[PBEffects::Encore]>0 + idxEncoredMove = battler.pbEncoredMoveIndex + return false if idxEncoredMove>=0 && idxMove!=idxEncoredMove + end + return battler.pbCanChooseMove?(move,true,showMessages,sleepTalk) + end + + def pbCanChooseAnyMove?(idxBattler,sleepTalk=false) + battler = @battlers[idxBattler] + battler.eachMoveWithIndex do |m,i| + next if m.pp==0 && m.total_pp>0 && !sleepTalk + if battler.effects[PBEffects::Encore]>0 + idxEncoredMove = battler.pbEncoredMoveIndex + next if idxEncoredMove>=0 && i!=idxEncoredMove + end + next if !battler.pbCanChooseMove?(m,true,false,sleepTalk) + return true + end + return false + end + + # Called when the Pokémon is Encored, or if it can't use any of its moves. + # Makes the Pokémon use the Encored move (if Encored), or Struggle. + def pbAutoChooseMove(idxBattler,showMessages=true) + battler = @battlers[idxBattler] + if battler.fainted? + pbClearChoice(idxBattler) + return true + end + # Encore + idxEncoredMove = battler.pbEncoredMoveIndex + if idxEncoredMove>=0 && pbCanChooseMove?(idxBattler,idxEncoredMove,false) + encoreMove = battler.moves[idxEncoredMove] + @choices[idxBattler][0] = :UseMove # "Use move" + @choices[idxBattler][1] = idxEncoredMove # Index of move to be used + @choices[idxBattler][2] = encoreMove # PokeBattle_Move object + @choices[idxBattler][3] = -1 # No target chosen yet + return true if singleBattle? + if pbOwnedByPlayer?(idxBattler) + if showMessages + pbDisplayPaused(_INTL("{1} has to use {2}!",battler.name,encoreMove.name)) + end + return pbChooseTarget(battler,encoreMove) + end + return true + end + # Struggle + if pbOwnedByPlayer?(idxBattler) && showMessages + pbDisplayPaused(_INTL("{1} has no moves left!",battler.name)) + end + @choices[idxBattler][0] = :UseMove # "Use move" + @choices[idxBattler][1] = -1 # Index of move to be used + @choices[idxBattler][2] = @struggle # Struggle PokeBattle_Move object + @choices[idxBattler][3] = -1 # No target chosen yet + return true + end + + def pbRegisterMove(idxBattler,idxMove,showMessages=true) + battler = @battlers[idxBattler] + move = battler.moves[idxMove] + return false if !pbCanChooseMove?(idxBattler,idxMove,showMessages) + @choices[idxBattler][0] = :UseMove # "Use move" + @choices[idxBattler][1] = idxMove # Index of move to be used + @choices[idxBattler][2] = move # PokeBattle_Move object + @choices[idxBattler][3] = -1 # No target chosen yet + return true + end + + def pbChoseMove?(idxBattler,moveID) + return false if !@battlers[idxBattler] || @battlers[idxBattler].fainted? + if @choices[idxBattler][0]==:UseMove && @choices[idxBattler][1] + return @choices[idxBattler][2].id == moveID + end + return false + end + + def pbChoseMoveFunctionCode?(idxBattler,code) + return false if @battlers[idxBattler].fainted? + if @choices[idxBattler][0]==:UseMove && @choices[idxBattler][1] + return @choices[idxBattler][2].function == code + end + return false + end + + def pbRegisterTarget(idxBattler,idxTarget) + @choices[idxBattler][3] = idxTarget # Set target of move + end + + # Returns whether the idxTarget will be targeted by a move with target_data + # used by a battler in idxUser. + def pbMoveCanTarget?(idxUser,idxTarget,target_data) + return false if target_data.num_targets == 0 + case target_data.id + when :NearAlly + return false if opposes?(idxUser,idxTarget) + return false if !nearBattlers?(idxUser,idxTarget) + when :UserOrNearAlly + return true if idxUser==idxTarget + return false if opposes?(idxUser,idxTarget) + return false if !nearBattlers?(idxUser,idxTarget) + when :UserAndAllies + return false if opposes?(idxUser,idxTarget) + when :NearFoe, :RandomNearFoe, :AllNearFoes + return false if !opposes?(idxUser,idxTarget) + return false if !nearBattlers?(idxUser,idxTarget) + when :Foe + return false if !opposes?(idxUser,idxTarget) + when :AllFoes + return false if !opposes?(idxUser,idxTarget) + when :NearOther, :AllNearOthers + return false if !nearBattlers?(idxUser,idxTarget) + when :Other + return false if idxUser==idxTarget + end + return true + end + + #============================================================================= + # Turn order calculation (priority) + #============================================================================= + def pbCalculatePriority(fullCalc=false,indexArray=nil) + needRearranging = false + if fullCalc + @priorityTrickRoom = (@field.effects[PBEffects::TrickRoom]>0) + # Recalculate everything from scratch + randomOrder = Array.new(maxBattlerIndex+1) { |i| i } + (randomOrder.length-1).times do |i| # Can't use shuffle! here + r = i+pbRandom(randomOrder.length-i) + randomOrder[i], randomOrder[r] = randomOrder[r], randomOrder[i] + end + @priority.clear + for i in 0..maxBattlerIndex + b = @battlers[i] + next if !b + # [battler, speed, sub-priority, priority, tie-breaker order] + bArray = [b,b.pbSpeed,0,0,randomOrder[i]] + if @choices[b.index][0]==:UseMove || @choices[b.index][0]==:Shift + # Calculate move's priority + if @choices[b.index][0]==:UseMove + move = @choices[b.index][2] + pri = move.priority + if b.abilityActive? + pri = BattleHandlers.triggerPriorityChangeAbility(b.ability,b,move,pri) + end + bArray[3] = pri + @choices[b.index][4] = pri + end + # Calculate sub-priority (first/last within priority bracket) + # NOTE: Going fast beats going slow. A Pokémon with Stall and Quick + # Claw will go first in its priority bracket if Quick Claw + # triggers, regardless of Stall. + subPri = 0 + # Abilities (Stall) + if b.abilityActive? + newSubPri = BattleHandlers.triggerPriorityBracketChangeAbility(b.ability, + b,subPri,self) + if subPri!=newSubPri + subPri = newSubPri + b.effects[PBEffects::PriorityAbility] = true + b.effects[PBEffects::PriorityItem] = false + end + end + # Items (Quick Claw, Custap Berry, Lagging Tail, Full Incense) + if b.itemActive? + newSubPri = BattleHandlers.triggerPriorityBracketChangeItem(b.item, + b,subPri,self) + if subPri!=newSubPri + subPri = newSubPri + b.effects[PBEffects::PriorityAbility] = false + b.effects[PBEffects::PriorityItem] = true + end + end + bArray[2] = subPri + end + @priority.push(bArray) + end + needRearranging = true + else + if (@field.effects[PBEffects::TrickRoom]>0)!=@priorityTrickRoom + needRearranging = true + @priorityTrickRoom = (@field.effects[PBEffects::TrickRoom]>0) + end + # Just recheck all battler speeds + @priority.each do |orderArray| + next if !orderArray + next if indexArray && !indexArray.include?(orderArray[0].index) + oldSpeed = orderArray[1] + orderArray[1] = orderArray[0].pbSpeed + needRearranging = true if orderArray[1]!=oldSpeed + end + end + # Reorder the priority array + if needRearranging + @priority.sort! { |a,b| + if a[3]!=b[3] + # Sort by priority (highest value first) + b[3]<=>a[3] + elsif a[2]!=b[2] + # Sort by sub-priority (highest value first) + b[2]<=>a[2] + elsif @priorityTrickRoom + # Sort by speed (lowest first), and use tie-breaker if necessary + (a[1]==b[1]) ? b[4]<=>a[4] : a[1]<=>b[1] + else + # Sort by speed (highest first), and use tie-breaker if necessary + (a[1]==b[1]) ? b[4]<=>a[4] : b[1]<=>a[1] + end + } + # Write the priority order to the debug log + logMsg = (fullCalc) ? "[Round order] " : "[Round order recalculated] " + comma = false + @priority.each do |orderArray| + logMsg += ", " if comma + logMsg += "#{orderArray[0].pbThis(comma)} (#{orderArray[0].index})" + comma = true + end + PBDebug.log(logMsg) + end + end + + def pbPriority(onlySpeedSort=false) + ret = [] + if onlySpeedSort + # Sort battlers by their speed stats and tie-breaker order only. + tempArray = [] + @priority.each { |pArray| tempArray.push([pArray[0],pArray[1],pArray[4]]) } + tempArray.sort! { |a,b| (a[1]==b[1]) ? b[2]<=>a[2] : b[1]<=>a[1] } + tempArray.each { |tArray| ret.push(tArray[0]) } + else + # Sort battlers by priority, sub-priority and their speed. Ties are + # resolved in the same way each time this method is called in a round. + @priority.each { |pArray| ret.push(pArray[0]) if !pArray[0].fainted? } + end + return ret + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/006_Battle_Action_Switching.rb b/Data/Scripts/011_Battle/003_Battle/006_Battle_Action_Switching.rb new file mode 100644 index 000000000..19359a448 --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/006_Battle_Action_Switching.rb @@ -0,0 +1,415 @@ +class PokeBattle_Battle + #============================================================================= + # Choosing Pokémon to switch + #============================================================================= + # Checks whether the replacement Pokémon (at party index idxParty) can enter + # battle. + # NOTE: Messages are only shown while in the party screen when choosing a + # command for the next round. + def pbCanSwitchLax?(idxBattler, idxParty, partyScene = nil) + return true if idxParty < 0 + party = pbParty(idxBattler) + return false if idxParty >= party.length + return false if !party[idxParty] + if party[idxParty].egg? + partyScene.pbDisplay(_INTL("An Egg can't battle!")) if partyScene + return false + end + if !pbIsOwner?(idxBattler, idxParty) + owner = pbGetOwnerFromPartyIndex(idxBattler, idxParty) + partyScene.pbDisplay(_INTL("You can't switch {1}'s Pokémon with one of yours!", + owner.name)) if partyScene + return false + end + if party[idxParty].fainted? + partyScene.pbDisplay(_INTL("{1} has no energy left to battle!", + party[idxParty].name)) if partyScene + return false + end + if pbFindBattler(idxParty, idxBattler) + partyScene.pbDisplay(_INTL("{1} is already in battle!", + party[idxParty].name)) if partyScene + return false + end + return true + end + + # Check whether the currently active Pokémon (at battler index idxBattler) can + # switch out (and that its replacement at party index idxParty can switch in). + # NOTE: Messages are only shown while in the party screen when choosing a + # command for the next round. + def pbCanSwitch?(idxBattler, idxParty = -1, partyScene = nil) + # Check whether party Pokémon can switch in + return false if !pbCanSwitchLax?(idxBattler, idxParty, partyScene) + # Make sure another battler isn't already choosing to switch to the party + # Pokémon + eachSameSideBattler(idxBattler) do |b| + next if choices[b.index][0] != :SwitchOut || choices[b.index][1] != idxParty + partyScene.pbDisplay(_INTL("{1} has already been selected.", + pbParty(idxBattler)[idxParty].name)) if partyScene + return false + end + # Check whether battler can switch out + battler = @battlers[idxBattler] + return true if battler.fainted? + # Ability/item effects that allow switching no matter what + if battler.abilityActive? + if BattleHandlers.triggerCertainSwitchingUserAbility(battler.ability, battler, self) + return true + end + end + if battler.itemActive? + if BattleHandlers.triggerCertainSwitchingUserItem(battler.item, battler, self) + return true + end + end + # Other certain switching effects + return true if Settings::MORE_TYPE_EFFECTS && battler.pbHasType?(:GHOST) + # Other certain trapping effects + if battler.effects[PBEffects::Trapping] > 0 || + battler.effects[PBEffects::MeanLook] >= 0 || + battler.effects[PBEffects::Ingrain] || + @field.effects[PBEffects::FairyLock] > 0 + partyScene.pbDisplay(_INTL("{1} can't be switched out!", battler.pbThis)) if partyScene + return false + end + # Trapping abilities/items + eachOtherSideBattler(idxBattler) do |b| + next if !b.abilityActive? + if BattleHandlers.triggerTrappingTargetAbility(b.ability, battler, b, self) + partyScene.pbDisplay(_INTL("{1}'s {2} prevents switching!", + b.pbThis, b.abilityName)) if partyScene + return false + end + end + eachOtherSideBattler(idxBattler) do |b| + next if !b.itemActive? + if BattleHandlers.triggerTrappingTargetItem(b.item, battler, b, self) + partyScene.pbDisplay(_INTL("{1}'s {2} prevents switching!", + b.pbThis, b.itemName)) if partyScene + return false + end + end + return true + end + + def pbCanChooseNonActive?(idxBattler) + pbParty(idxBattler).each_with_index do |_pkmn, i| + return true if pbCanSwitchLax?(idxBattler, i) + end + return false + end + + def pbRegisterSwitch(idxBattler, idxParty) + return false if !pbCanSwitch?(idxBattler, idxParty) + @choices[idxBattler][0] = :SwitchOut + @choices[idxBattler][1] = idxParty # Party index of Pokémon to switch in + @choices[idxBattler][2] = nil + return true + end + + #============================================================================= + # Open the party screen and potentially pick a replacement Pokémon (or AI + # chooses replacement) + #============================================================================= + # Open party screen and potentially choose a Pokémon to switch with. Used in + # all instances where the party screen is opened. + def pbPartyScreen(idxBattler, checkLaxOnly = false, canCancel = false, shouldRegister = false) + ret = -1 + @scene.pbPartyScreen(idxBattler, canCancel) { |idxParty, partyScene| + if checkLaxOnly + next false if !pbCanSwitchLax?(idxBattler, idxParty, partyScene) + else + next false if !pbCanSwitch?(idxBattler, idxParty, partyScene) + end + if shouldRegister + next false if idxParty < 0 || !pbRegisterSwitch(idxBattler, idxParty) + end + ret = idxParty + next true + } + return ret + end + + # For choosing a replacement Pokémon when prompted in the middle of other + # things happening (U-turn, Baton Pass, in def pbSwitch). + def pbSwitchInBetween(idxBattler, checkLaxOnly = false, canCancel = false) + return pbPartyScreen(idxBattler, checkLaxOnly, canCancel) if pbOwnedByPlayer?(idxBattler) + return @battleAI.pbDefaultChooseNewEnemy(idxBattler, pbParty(idxBattler)) + end + + #============================================================================= + # Switching Pokémon + #============================================================================= + # General switching method that checks if any Pokémon need to be sent out and, + # if so, does. Called at the end of each round. + def pbEORSwitch(favorDraws = false) + return if @decision > 0 && !favorDraws + return if @decision == 5 && favorDraws + pbJudge + return if @decision > 0 + # Check through each fainted battler to see if that spot can be filled. + switched = [] + loop do + switched.clear + @battlers.each do |b| + next if !b || !b.fainted? + idxBattler = b.index + next if !pbCanChooseNonActive?(idxBattler) + if !pbOwnedByPlayer?(idxBattler) # Opponent/ally is switching in + next if wildBattle? && opposes?(idxBattler) # Wild Pokémon can't switch + idxPartyNew = pbSwitchInBetween(idxBattler) + opponent = pbGetOwnerFromBattlerIndex(idxBattler) + # NOTE: The player is only offered the chance to switch their own + # Pokémon when an opponent replaces a fainted Pokémon in single + # battles. In double battles, etc. there is no such offer. + if @internalBattle && @switchStyle && trainerBattle? && pbSideSize(0) == 1 && + opposes?(idxBattler) && !@battlers[0].fainted? && !switched.include?(0) && + pbCanChooseNonActive?(0) && @battlers[0].effects[PBEffects::Outrage] == 0 + idxPartyForName = idxPartyNew + enemyParty = pbParty(idxBattler) + if enemyParty[idxPartyNew].ability == :ILLUSION + new_index = pbLastInTeam(idxBattler) + idxPartyForName = new_index if new_index >= 0 && new_index != idxPartyNew + end + switchMessageHard = _INTL("{1} is about to send in a new Pokémon. Will you switch your Pokémon?", opponent.fullname) + switchMessageNormal = _INTL("{1} is about to send in {2}. Will you switch your Pokémon?", opponent.full_name, enemyParty[idxPartyForName].name) + switchMessage = $game_switches[SWITCH_GAME_DIFFICULTY_HARD] ? switchMessageHard : switchMessageNormal + if pbDisplayConfirm(switchMessage) + idxPlayerPartyNew = pbSwitchInBetween(0, false, true) + if idxPlayerPartyNew >= 0 + pbMessageOnRecall(@battlers[0]) + pbRecallAndReplace(0, idxPlayerPartyNew) + switched.push(0) + end + end + end + pbRecallAndReplace(idxBattler, idxPartyNew) + switched.push(idxBattler) + elsif trainerBattle? # Player switches in in a trainer battle + idxPlayerPartyNew = pbGetReplacementPokemonIndex(idxBattler) # Owner chooses + pbRecallAndReplace(idxBattler, idxPlayerPartyNew) + switched.push(idxBattler) + else + # Player's Pokémon has fainted in a wild battle + switch = false + if !pbDisplayConfirm(_INTL("Use next Pokémon?")) + switch = (pbRun(idxBattler, true) <= 0) + else + switch = true + end + if switch + idxPlayerPartyNew = pbGetReplacementPokemonIndex(idxBattler) # Owner chooses + pbRecallAndReplace(idxBattler, idxPlayerPartyNew) + switched.push(idxBattler) + end + end + end + break if switched.length == 0 + pbPriority(true).each do |b| + b.pbEffectsOnSwitchIn(true) if switched.include?(b.index) + end + end + end + + def pbGetReplacementPokemonIndex(idxBattler, random = false) + if random + return -1 if !pbCanSwitch?(idxBattler) # Can battler switch out? + choices = [] # Find all Pokémon that can switch in + eachInTeamFromBattlerIndex(idxBattler) do |_pkmn, i| + choices.push(i) if pbCanSwitchLax?(idxBattler, i) + end + return -1 if choices.length == 0 + return choices[pbRandom(choices.length)] + else + return pbSwitchInBetween(idxBattler, true) + end + end + + # Actually performs the recalling and sending out in all situations. + def pbRecallAndReplace(idxBattler, idxParty, randomReplacement = false, batonPass = false) + @scene.pbRecall(idxBattler) if !@battlers[idxBattler].fainted? + @battlers[idxBattler].pbAbilitiesOnSwitchOut # Inc. primordial weather check + @scene.pbShowPartyLineup(idxBattler & 1) if pbSideSize(idxBattler) == 1 + pbMessagesOnReplace(idxBattler, idxParty) if !randomReplacement + pbReplace(idxBattler, idxParty, batonPass) + end + + def pbMessageOnRecall(battler) + if battler.pbOwnedByPlayer? + if battler.hp <= battler.totalhp / 4 + pbDisplayBrief(_INTL("Good job, {1}! Come back!", battler.name)) + elsif battler.hp <= battler.totalhp / 2 + pbDisplayBrief(_INTL("OK, {1}! Come back!", battler.name)) + elsif battler.turnCount >= 5 + pbDisplayBrief(_INTL("{1}, that's enough! Come back!", battler.name)) + elsif battler.turnCount >= 2 + pbDisplayBrief(_INTL("{1}, come back!", battler.name)) + else + pbDisplayBrief(_INTL("{1}, switch out! Come back!", battler.name)) + end + else + owner = pbGetOwnerName(battler.index) + pbDisplayBrief(_INTL("{1} withdrew {2}!", owner, battler.name)) + end + end + + # Only called from def pbRecallAndReplace and Battle Arena's def pbSwitch. + def pbMessagesOnReplace(idxBattler, idxParty) + party = pbParty(idxBattler) + newPkmnName = party[idxParty].name + if party[idxParty].ability == :ILLUSION + new_index = pbLastInTeam(idxBattler) + newPkmnName = party[new_index].name if new_index >= 0 && new_index != idxParty + end + if pbOwnedByPlayer?(idxBattler) + opposing = @battlers[idxBattler].pbDirectOpposing + if opposing.fainted? || opposing.hp == opposing.totalhp + pbDisplayBrief(_INTL("You're in charge, {1}!", newPkmnName)) + elsif opposing.hp >= opposing.totalhp / 2 + pbDisplayBrief(_INTL("Go for it, {1}!", newPkmnName)) + elsif opposing.hp >= opposing.totalhp / 4 + pbDisplayBrief(_INTL("Just a little more! Hang in there, {1}!", newPkmnName)) + else + pbDisplayBrief(_INTL("Your opponent's weak! Get 'em, {1}!", newPkmnName)) + end + else + owner = pbGetOwnerFromBattlerIndex(idxBattler) + pbDisplayBrief(_INTL("{1} sent out {2}!", owner.full_name, newPkmnName)) + end + end + + # Only called from def pbRecallAndReplace above and Battle Arena's def + # pbSwitch. + def pbReplace(idxBattler, idxParty, batonPass = false) + party = pbParty(idxBattler) + idxPartyOld = @battlers[idxBattler].pokemonIndex + # Initialise the new Pokémon + @battlers[idxBattler].pbInitialize(party[idxParty], idxParty, batonPass) + # Reorder the party for this battle + partyOrder = pbPartyOrder(idxBattler) + partyOrder[idxParty], partyOrder[idxPartyOld] = partyOrder[idxPartyOld], partyOrder[idxParty] + # Send out the new Pokémon + pbSendOut([[idxBattler, party[idxParty]]]) + pbCalculatePriority(false, [idxBattler]) if Settings::RECALCULATE_TURN_ORDER_AFTER_SPEED_CHANGES + end + + # Called from def pbReplace above and at the start of battle. + # sendOuts is an array; each element is itself an array: [idxBattler,pkmn] + def pbSendOut(sendOuts, startBattle = false) + sendOuts.each { |b| @peer.pbOnEnteringBattle(self, b[1]) } + @scene.pbSendOutBattlers(sendOuts, startBattle) + sendOuts.each do |b| + @scene.pbResetMoveIndex(b[0]) + pbSetSeen(@battlers[b[0]]) + @usedInBattle[b[0] & 1][b[0] / 2] = true + end + end + + #============================================================================= + # Effects upon a Pokémon entering battle + #============================================================================= + # Called at the start of battle only. + def pbOnActiveAll + # Weather-inducing abilities, Trace, Imposter, etc. + pbCalculatePriority(true) + pbPriority(true).each { |b| b.pbEffectsOnSwitchIn(true) } + pbCalculatePriority + # Check forms are correct + eachBattler { |b| b.pbCheckForm } + end + + # Called when a Pokémon switches in (entry effects, entry hazards). + def pbOnActiveOne(battler) + return false if battler.fainted? + # Introduce Shadow Pokémon + if battler.opposes? && battler.shadowPokemon? + pbCommonAnimation("Shadow", battler) + pbDisplay(_INTL("Oh!\nA Shadow Pokémon!")) + end + # Record money-doubling effect of Amulet Coin/Luck Incense + if !battler.opposes? && [:AMULETCOIN, :LUCKINCENSE].include?(battler.item_id) + @field.effects[PBEffects::AmuletCoin] = true + end + # Update battlers' participants (who will gain Exp/EVs when a battler faints) + eachBattler { |b| b.pbUpdateParticipants } + # Healing Wish + if @positions[battler.index].effects[PBEffects::HealingWish] + pbCommonAnimation("HealingWish", battler) + pbDisplay(_INTL("The healing wish came true for {1}!", battler.pbThis(true))) + battler.pbRecoverHP(battler.totalhp) + battler.pbCureStatus(false) + @positions[battler.index].effects[PBEffects::HealingWish] = false + end + # Lunar Dance + if @positions[battler.index].effects[PBEffects::LunarDance] + pbCommonAnimation("LunarDance", battler) + pbDisplay(_INTL("{1} became cloaked in mystical moonlight!", battler.pbThis)) + battler.pbRecoverHP(battler.totalhp) + battler.pbCureStatus(false) + battler.eachMove { |m| m.pp = m.total_pp } + @positions[battler.index].effects[PBEffects::LunarDance] = false + end + # Entry hazards + # Stealth Rock + if battler.pbOwnSide.effects[PBEffects::StealthRock] && battler.takesIndirectDamage? && + GameData::Type.exists?(:ROCK) + bTypes = battler.pbTypes(true) + eff = Effectiveness.calculate(:ROCK, bTypes[0], bTypes[1], bTypes[2]) + if !Effectiveness.ineffective?(eff) + eff = eff.to_f / Effectiveness::NORMAL_EFFECTIVE + oldHP = battler.hp + battler.pbReduceHP(battler.totalhp * eff / 8, false) + pbDisplay(_INTL("Pointed stones dug into {1}!", battler.pbThis)) + battler.pbItemHPHealCheck + if battler.pbAbilitiesOnDamageTaken(oldHP) # Switched out + return pbOnActiveOne(battler) # For replacement battler + end + end + end + # Spikes + if battler.pbOwnSide.effects[PBEffects::Spikes] > 0 && battler.takesIndirectDamage? && + !battler.airborne? + spikesDiv = [8, 6, 4][battler.pbOwnSide.effects[PBEffects::Spikes] - 1] + oldHP = battler.hp + battler.pbReduceHP(battler.totalhp / spikesDiv, false) + pbDisplay(_INTL("{1} is hurt by the spikes!", battler.pbThis)) + battler.pbItemHPHealCheck + if battler.pbAbilitiesOnDamageTaken(oldHP) # Switched out + return pbOnActiveOne(battler) # For replacement battler + end + end + # Toxic Spikes + if battler.pbOwnSide.effects[PBEffects::ToxicSpikes] > 0 && !battler.fainted? && + !battler.airborne? + if battler.pbHasType?(:POISON) + battler.pbOwnSide.effects[PBEffects::ToxicSpikes] = 0 + pbDisplay(_INTL("{1} absorbed the poison spikes!", battler.pbThis)) + elsif battler.pbCanPoison?(nil, false) + if battler.pbOwnSide.effects[PBEffects::ToxicSpikes] == 2 + battler.pbPoison(nil, _INTL("{1} was badly poisoned by the poison spikes!", battler.pbThis), true) + else + battler.pbPoison(nil, _INTL("{1} was poisoned by the poison spikes!", battler.pbThis)) + end + end + end + # Sticky Web + if battler.pbOwnSide.effects[PBEffects::StickyWeb] && !battler.fainted? && + !battler.airborne? + pbDisplay(_INTL("{1} was caught in a sticky web!", battler.pbThis)) + if battler.pbCanLowerStatStage?(:SPEED) + battler.pbLowerStatStage(:SPEED, 1, nil) + battler.pbItemStatRestoreCheck + end + end + # Battler faints if it is knocked out because of an entry hazard above + if battler.fainted? + battler.pbFaint + pbGainExp + pbJudge + return false + end + battler.pbCheckForm + return true + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/007_Battle_Action_UseItem.rb b/Data/Scripts/011_Battle/003_Battle/007_Battle_Action_UseItem.rb new file mode 100644 index 000000000..801583ea6 --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/007_Battle_Action_UseItem.rb @@ -0,0 +1,148 @@ +class PokeBattle_Battle + #============================================================================= + # Choosing to use an item + #============================================================================= + def pbCanUseItemOnPokemon?(item,pkmn,battler,scene,showMessages=true) + if !pkmn || pkmn.egg? + scene.pbDisplay(_INTL("It won't have any effect.")) if showMessages + return false + end + # Embargo + if battler && battler.effects[PBEffects::Embargo]>0 + scene.pbDisplay(_INTL("Embargo's effect prevents the item's use on {1}!", + battler.pbThis(true))) if showMessages + return false + end + return true + end + + # NOTE: Using a Poké Ball consumes all your actions for the round. The method + # below is one half of making this happen; the other half is in the + # ItemHandlers::CanUseInBattle for Poké Balls. + def pbItemUsesAllActions?(item) + return true if GameData::Item.get(item).is_poke_ball? + return false + end + + def pbRegisterItem(idxBattler,item,idxTarget=nil,idxMove=nil) + # Register for use of item on a Pokémon in the party + @choices[idxBattler][0] = :UseItem + @choices[idxBattler][1] = item # ID of item to be used + @choices[idxBattler][2] = idxTarget # Party index of Pokémon to use item on + @choices[idxBattler][3] = idxMove # Index of move to recharge (Ethers) + # Delete the item from the Bag. If it turns out it will have no effect, it + # will be re-added to the Bag later. + pbConsumeItemInBag(item,idxBattler) + return true + end + + #============================================================================= + # Using an item + #============================================================================= + def pbConsumeItemInBag(item,idxBattler) + return if !item + useType = GameData::Item.get(item).battle_use + return if useType==0 || (useType>=6 && useType<=10) # Not consumed upon use + if pbOwnedByPlayer?(idxBattler) + if !$PokemonBag.pbDeleteItem(item) + raise _INTL("Tried to consume item that wasn't in the Bag somehow.") + end + else + items = pbGetOwnerItems(idxBattler) + items.each_with_index do |thisItem,i| + next if thisItem!=item + items[i] = nil + break + end + items.compact! + end + end + + def pbReturnUnusedItemToBag(item,idxBattler) + return if !item + useType = GameData::Item.get(item).battle_use + return if useType==0 || (useType>=6 && useType<=10) # Not consumed upon use + if pbOwnedByPlayer?(idxBattler) + if $PokemonBag && $PokemonBag.pbCanStore?(item) + $PokemonBag.pbStoreItem(item) + else + raise _INTL("Couldn't return unused item to Bag somehow.") + end + else + items = pbGetOwnerItems(idxBattler) + items.push(item) if items + end + end + + def pbUseItemMessage(item,trainerName) + itemName = GameData::Item.get(item).name + if itemName.starts_with_vowel? + pbDisplayBrief(_INTL("{1} used an {2}.",trainerName,itemName)) + else + pbDisplayBrief(_INTL("{1} used a {2}.",trainerName,itemName)) + end + end + + # Uses an item on a Pokémon in the trainer's party. + def pbUseItemOnPokemon(item,idxParty,userBattler) + trainerName = pbGetOwnerName(userBattler.index) + pbUseItemMessage(item,trainerName) + pkmn = pbParty(userBattler.index)[idxParty] + battler = pbFindBattler(idxParty,userBattler.index) + ch = @choices[userBattler.index] + if ItemHandlers.triggerCanUseInBattle(item,pkmn,battler,ch[3],true,self,@scene,false) + ItemHandlers.triggerBattleUseOnPokemon(item,pkmn,battler,ch,@scene) + ch[1] = nil # Delete item from choice + return + end + pbDisplay(_INTL("But it had no effect!")) + # Return unused item to Bag + pbReturnUnusedItemToBag(item,userBattler.index) + end + + # Uses an item on a Pokémon in battle that belongs to the trainer. + def pbUseItemOnBattler(item,idxParty,userBattler) + trainerName = pbGetOwnerName(userBattler.index) + pbUseItemMessage(item,trainerName) + battler = pbFindBattler(idxParty,userBattler.index) + ch = @choices[userBattler.index] + if battler + if ItemHandlers.triggerCanUseInBattle(item,battler.pokemon,battler,ch[3],true,self,@scene,false) + ItemHandlers.triggerBattleUseOnBattler(item,battler,@scene) + ch[1] = nil # Delete item from choice + return + else + pbDisplay(_INTL("But it had no effect!")) + end + else + pbDisplay(_INTL("But it's not where this item can be used!")) + end + # Return unused item to Bag + pbReturnUnusedItemToBag(item,userBattler.index) + end + + # Uses a Poké Ball in battle directly. + def pbUsePokeBallInBattle(item,idxBattler,userBattler) + idxBattler = userBattler.index if idxBattler<0 + battler = @battlers[idxBattler] + ItemHandlers.triggerUseInBattle(item,battler,self) + @choices[userBattler.index][1] = nil # Delete item from choice + end + + # Uses an item in battle directly. + def pbUseItemInBattle(item,idxBattler,userBattler) + trainerName = pbGetOwnerName(userBattler.index) + pbUseItemMessage(item,trainerName) + battler = (idxBattler<0) ? userBattler : @battlers[idxBattler] + pkmn = battler.pokemon + ch = @choices[userBattler.index] + if ItemHandlers.triggerCanUseInBattle(item,pkmn,battler,ch[3],true,self,@scene,false) + ItemHandlers.triggerUseInBattle(item,battler,self) + ch[1] = nil # Delete item from choice + return + end + pbDisplay(_INTL("But it had no effect!")) + # Return unused item to Bag + pbReturnUnusedItemToBag(item,userBattler.index) + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/008_Battle_Action_Running.rb b/Data/Scripts/011_Battle/003_Battle/008_Battle_Action_Running.rb new file mode 100644 index 000000000..e26fc9aa2 --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/008_Battle_Action_Running.rb @@ -0,0 +1,156 @@ +class PokeBattle_Battle + #============================================================================= + # Running from battle + #============================================================================= + def pbCanRun?(idxBattler) + return false if trainerBattle? + battler = @battlers[idxBattler] + return false if !@canRun && !battler.opposes? + return true if battler.pbHasType?(:GHOST) && Settings::MORE_TYPE_EFFECTS + return true if battler.abilityActive? && + BattleHandlers.triggerRunFromBattleAbility(battler.ability,battler) + return true if battler.itemActive? && + BattleHandlers.triggerRunFromBattleItem(battler.item,battler) + return false if battler.effects[PBEffects::Trapping]>0 || + battler.effects[PBEffects::MeanLook]>=0 || + battler.effects[PBEffects::Ingrain] || + @field.effects[PBEffects::FairyLock]>0 + eachOtherSideBattler(idxBattler) do |b| + return false if b.abilityActive? && + BattleHandlers.triggerTrappingTargetAbility(b.ability,battler,b,self) + return false if b.itemActive? && + BattleHandlers.triggerTrappingTargetItem(b.item,battler,b,self) + end + return true + end + + # Return values: + # -1: Failed fleeing + # 0: Wasn't possible to attempt fleeing, continue choosing action for the round + # 1: Succeeded at fleeing, battle will end + # duringBattle is true for replacing a fainted Pokémon during the End Of Round + # phase, and false for choosing the Run command. + def pbRun(idxBattler,duringBattle=false) + battler = @battlers[idxBattler] + if battler.opposes? + return 0 if trainerBattle? + @choices[idxBattler][0] = :Run + @choices[idxBattler][1] = 0 + @choices[idxBattler][2] = nil + return -1 + end + # Fleeing from trainer battles + if trainerBattle? + if $DEBUG && Input.press?(Input::CTRL) + if pbDisplayConfirm(_INTL("Treat this battle as a win?")) + @decision = 1 + return 1 + elsif pbDisplayConfirm(_INTL("Treat this battle as a loss?")) + @decision = 2 + return 1 + end + elsif @internalBattle + if pbDisplayConfirm(_INTL("Would you like to forfeit the match and quit now?")) + pbSEPlay("Battle flee") + pbDisplay(_INTL("{1} forfeited the match!",self.pbPlayer.name)) + @decision = 2 + return 1 + end + elsif pbDisplayConfirm(_INTL("Would you like to forfeit the match and quit now?")) + pbSEPlay("Battle flee") + pbDisplay(_INTL("{1} forfeited the match!",self.pbPlayer.name)) + @decision = 3 + return 1 + end + return 0 + end + # Fleeing from wild battles + if $DEBUG && Input.press?(Input::CTRL) + pbSEPlay("Battle flee") + pbDisplayPaused(_INTL("You got away safely!")) + @decision = 3 + return 1 + end + if !@canRun + pbDisplayPaused(_INTL("You can't escape!")) + return 0 + end + if !duringBattle + if battler.pbHasType?(:GHOST) && Settings::MORE_TYPE_EFFECTS + pbSEPlay("Battle flee") + pbDisplayPaused(_INTL("You got away safely!")) + @decision = 3 + return 1 + end + # Abilities that guarantee escape + if battler.abilityActive? + if BattleHandlers.triggerRunFromBattleAbility(battler.ability,battler) + pbShowAbilitySplash(battler,true) + pbHideAbilitySplash(battler) + pbSEPlay("Battle flee") + pbDisplayPaused(_INTL("You got away safely!")) + @decision = 3 + return 1 + end + end + # Held items that guarantee escape + if battler.itemActive? + if BattleHandlers.triggerRunFromBattleItem(battler.item,battler) + pbSEPlay("Battle flee") + pbDisplayPaused(_INTL("{1} fled using its {2}!", + battler.pbThis,battler.itemName)) + @decision = 3 + return 1 + end + end + # Other certain trapping effects + if battler.effects[PBEffects::Trapping]>0 || + battler.effects[PBEffects::MeanLook]>=0 || + battler.effects[PBEffects::Ingrain] || + @field.effects[PBEffects::FairyLock]>0 + pbDisplayPaused(_INTL("You can't escape!")) + return 0 + end + # Trapping abilities/items + eachOtherSideBattler(idxBattler) do |b| + next if !b.abilityActive? + if BattleHandlers.triggerTrappingTargetAbility(b.ability,battler,b,self) + pbDisplayPaused(_INTL("{1} prevents escape with {2}!",b.pbThis,b.abilityName)) + return 0 + end + end + eachOtherSideBattler(idxBattler) do |b| + next if !b.itemActive? + if BattleHandlers.triggerTrappingTargetItem(b.item,battler,b,self) + pbDisplayPaused(_INTL("{1} prevents escape with {2}!",b.pbThis,b.itemName)) + return 0 + end + end + end + # Fleeing calculation + # Get the speeds of the Pokémon fleeing and the fastest opponent + # NOTE: Not pbSpeed, because using unmodified Speed. + @runCommand += 1 if !duringBattle # Make it easier to flee next time + speedPlayer = @battlers[idxBattler].speed + speedEnemy = 1 + eachOtherSideBattler(idxBattler) do |b| + speed = b.speed + speedEnemy = speed if speedEnemyspeedEnemy + rate = 256 + else + rate = (speedPlayer*128)/speedEnemy + rate += @runCommand*30 + end + if rate>=256 || @battleAI.pbAIRandom(256)=0 + # return false if !pbHasMegaRing?(idxBattler) + # side = @battlers[idxBattler].idxOwnSide + # owner = pbGetOwnerIndexFromBattlerIndex(idxBattler) + # return @megaEvolution[side][owner]==-1 + end + + def pbRegisterMegaEvolution(idxBattler) + side = @battlers[idxBattler].idxOwnSide + owner = pbGetOwnerIndexFromBattlerIndex(idxBattler) + @megaEvolution[side][owner] = idxBattler + end + + def pbUnregisterMegaEvolution(idxBattler) + side = @battlers[idxBattler].idxOwnSide + owner = pbGetOwnerIndexFromBattlerIndex(idxBattler) + @megaEvolution[side][owner] = -1 if @megaEvolution[side][owner]==idxBattler + end + + def pbToggleRegisteredMegaEvolution(idxBattler) + side = @battlers[idxBattler].idxOwnSide + owner = pbGetOwnerIndexFromBattlerIndex(idxBattler) + if @megaEvolution[side][owner]==idxBattler + @megaEvolution[side][owner] = -1 + else + @megaEvolution[side][owner] = idxBattler + end + end + + def pbRegisteredMegaEvolution?(idxBattler) + side = @battlers[idxBattler].idxOwnSide + owner = pbGetOwnerIndexFromBattlerIndex(idxBattler) + return @megaEvolution[side][owner]==idxBattler + end + + #============================================================================= + # Mega Evolving a battler + #============================================================================= + def pbMegaEvolve(idxBattler) + battler = @battlers[idxBattler] + return if !battler || !battler.pokemon + return if !battler.hasMega? || battler.mega? + trainerName = pbGetOwnerName(idxBattler) + # Break Illusion + if battler.hasActiveAbility?(:ILLUSION) + BattleHandlers.triggerTargetAbilityOnHit(battler.ability,nil,battler,nil,self) + end + # Mega Evolve + case battler.pokemon.megaMessage + when 1 # Rayquaza + pbDisplay(_INTL("{1}'s fervent wish has reached {2}!",trainerName,battler.pbThis)) + else + pbDisplay(_INTL("{1}'s {2} is reacting to {3}'s {4}!", + battler.pbThis,battler.itemName,trainerName,pbGetMegaRingName(idxBattler))) + end + pbCommonAnimation("MegaEvolution",battler) + battler.pokemon.makeMega + battler.form = battler.pokemon.form + battler.pbUpdate(true) + @scene.pbChangePokemon(battler,battler.pokemon) + @scene.pbRefreshOne(idxBattler) + pbCommonAnimation("MegaEvolution2",battler) + megaName = battler.pokemon.megaName + megaName = _INTL("Mega {1}", battler.pokemon.speciesName) if nil_or_empty?(megaName) + pbDisplay(_INTL("{1} has Mega Evolved into {2}!",battler.pbThis,megaName)) + side = battler.idxOwnSide + owner = pbGetOwnerIndexFromBattlerIndex(idxBattler) + @megaEvolution[side][owner] = -2 + if battler.isSpecies?(:GENGAR) && battler.mega? + battler.effects[PBEffects::Telekinesis] = 0 + end + pbCalculatePriority(false,[idxBattler]) if Settings::RECALCULATE_TURN_ORDER_AFTER_MEGA_EVOLUTION + # Trigger ability + battler.pbEffectsOnSwitchIn + end + + #============================================================================= + # Primal Reverting a battler + #============================================================================= + def pbPrimalReversion(idxBattler) + battler = @battlers[idxBattler] + return if !battler || !battler.pokemon + return if !battler.hasPrimal? || battler.primal? + if battler.isSpecies?(:KYOGRE) + pbCommonAnimation("PrimalKyogre",battler) + elsif battler.isSpecies?(:GROUDON) + pbCommonAnimation("PrimalGroudon",battler) + end + battler.pokemon.makePrimal + battler.form = battler.pokemon.form + battler.pbUpdate(true) + @scene.pbChangePokemon(battler,battler.pokemon) + @scene.pbRefreshOne(idxBattler) + if battler.isSpecies?(:KYOGRE) + pbCommonAnimation("PrimalKyogre2",battler) + elsif battler.isSpecies?(:GROUDON) + pbCommonAnimation("PrimalGroudon2",battler) + end + pbDisplay(_INTL("{1}'s Primal Reversion!\nIt reverted to its primal form!",battler.pbThis)) + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/010_Battle_Phase_Command.rb b/Data/Scripts/011_Battle/003_Battle/010_Battle_Phase_Command.rb new file mode 100644 index 000000000..a5529ae4d --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/010_Battle_Phase_Command.rb @@ -0,0 +1,253 @@ +class PokeBattle_Battle + #============================================================================= + # Clear commands + #============================================================================= + def pbClearChoice(idxBattler) + @choices[idxBattler] = [] if !@choices[idxBattler] + @choices[idxBattler][0] = :None + @choices[idxBattler][1] = 0 + @choices[idxBattler][2] = nil + @choices[idxBattler][3] = -1 + end + + def pbCancelChoice(idxBattler) + # If idxBattler's choice was to use an item, return that item to the Bag + if @choices[idxBattler][0] == :UseItem + item = @choices[idxBattler][1] + pbReturnUnusedItemToBag(item, idxBattler) if item + end + # If idxBattler chose to Mega Evolve, cancel it + pbUnregisterMegaEvolution(idxBattler) + # Clear idxBattler's choice + pbClearChoice(idxBattler) + end + + #============================================================================= + # Use main command menu (Fight/Pokémon/Bag/Run) + #============================================================================= + def pbCommandMenu(idxBattler,firstAction) + return @scene.pbCommandMenu(idxBattler,firstAction) + end + + #============================================================================= + # Check whether actions can be taken + #============================================================================= + def pbCanShowCommands?(idxBattler) + battler = @battlers[idxBattler] + return false if !battler || battler.fainted? + return false if battler.usingMultiTurnAttack? + return true + end + + def pbCanShowFightMenu?(idxBattler) + battler = @battlers[idxBattler] + # Encore + return false if battler.effects[PBEffects::Encore]>0 + # No moves that can be chosen (will Struggle instead) + usable = false + battler.eachMoveWithIndex do |_m,i| + next if !pbCanChooseMove?(idxBattler,i,false) + usable = true + break + end + return usable + end + + #============================================================================= + # Use sub-menus to choose an action, and register it if is allowed + #============================================================================= + # Returns true if a choice was made, false if cancelled. + def pbFightMenu(idxBattler) + # Auto-use Encored move or no moves choosable, so auto-use Struggle + return pbAutoChooseMove(idxBattler) if !pbCanShowFightMenu?(idxBattler) + # Battle Palace only + return true if pbAutoFightMenu(idxBattler) + # Regular move selection + ret = false + @scene.pbFightMenu(idxBattler,pbCanMegaEvolve?(idxBattler)) { |cmd| + case cmd + when -1 # Cancel + when -2 # Toggle Mega Evolution + pbToggleRegisteredMegaEvolution(idxBattler) + next false + when -3 # Shift + pbUnregisterMegaEvolution(idxBattler) + pbRegisterShift(idxBattler) + ret = true + else # Chose a move to use + next false if cmd<0 || !@battlers[idxBattler].moves[cmd] || + !@battlers[idxBattler].moves[cmd].id + next false if !pbRegisterMove(idxBattler,cmd) + next false if !singleBattle? && + !pbChooseTarget(@battlers[idxBattler],@battlers[idxBattler].moves[cmd]) + ret = true + end + next true + } + return ret + end + + def pbAutoFightMenu(idxBattler); return false; end + + def pbChooseTarget(battler,move) + target_data = move.pbTarget(battler) + idxTarget = @scene.pbChooseTarget(battler.index,target_data) + return false if idxTarget<0 + pbRegisterTarget(battler.index,idxTarget) + return true + end + + def pbItemMenu(idxBattler,firstAction) + if !@internalBattle + pbDisplay(_INTL("Items can't be used here.")) + return false + end + ret = false + @scene.pbItemMenu(idxBattler,firstAction) { |item,useType,idxPkmn,idxMove,itemScene| + next false if !item + battler = pkmn = nil + case useType + when 1, 2, 6, 7 # Use on Pokémon/Pokémon's move + next false if !ItemHandlers.hasBattleUseOnPokemon(item) + battler = pbFindBattler(idxPkmn,idxBattler) + pkmn = pbParty(idxBattler)[idxPkmn] + next false if !pbCanUseItemOnPokemon?(item,pkmn,battler,itemScene) + when 3, 8 # Use on battler + next false if !ItemHandlers.hasBattleUseOnBattler(item) + battler = pbFindBattler(idxPkmn,idxBattler) + pkmn = battler.pokemon if battler + next false if !pbCanUseItemOnPokemon?(item,pkmn,battler,itemScene) + when 4, 9 # Poké Balls + next false if idxPkmn<0 + battler = @battlers[idxPkmn] + pkmn = battler.pokemon if battler + when 5, 10 # No target (Poké Doll, Guard Spec., Launcher items) + battler = @battlers[idxBattler] + pkmn = battler.pokemon if battler + else + next false + end + next false if !pkmn + next false if !ItemHandlers.triggerCanUseInBattle(item, + pkmn,battler,idxMove,firstAction,self,itemScene) + next false if !pbRegisterItem(idxBattler,item,idxPkmn,idxMove) + ret = true + next true + } + return ret + end + + def pbPartyMenu(idxBattler) + ret = -1 + if @debug + ret = @battleAI.pbDefaultChooseNewEnemy(idxBattler,pbParty(idxBattler)) + else + ret = pbPartyScreen(idxBattler,false,true,true) + end + return ret>=0 + end + + def pbRunMenu(idxBattler) + # Regardless of succeeding or failing to run, stop choosing actions + return pbRun(idxBattler)!=0 + end + + def pbCallMenu(idxBattler) + return pbRegisterCall(idxBattler) + end + + def pbDebugMenu + # NOTE: This doesn't do anything yet. Maybe you can write your own debugging + # options! + end + + #============================================================================= + # Command phase + #============================================================================= + def pbCommandPhase + @scene.pbBeginCommandPhase + # Reset choices if commands can be shown + @battlers.each_with_index do |b,i| + next if !b + pbClearChoice(i) if pbCanShowCommands?(i) + end + # Reset choices to perform Mega Evolution if it wasn't done somehow + for side in 0...2 + @megaEvolution[side].each_with_index do |megaEvo,i| + @megaEvolution[side][i] = -1 if megaEvo>=0 + end + end + # Choose actions for the round (player first, then AI) + pbCommandPhaseLoop(true) # Player chooses their actions + return if @decision!=0 # Battle ended, stop choosing actions + pbCommandPhaseLoop(false) # AI chooses their actions + end + + def pbCommandPhaseLoop(isPlayer) + # NOTE: Doing some things (e.g. running, throwing a Poké Ball) takes up all + # your actions in a round. + actioned = [] + idxBattler = -1 + loop do + break if @decision!=0 # Battle ended, stop choosing actions + idxBattler += 1 + break if idxBattler>=@battlers.length + next if !@battlers[idxBattler] || pbOwnedByPlayer?(idxBattler)!=isPlayer + next if @choices[idxBattler][0]!=:None # Action is forced, can't choose one + next if !pbCanShowCommands?(idxBattler) # Action is forced, can't choose one + # AI controls this battler + if @controlPlayer || !pbOwnedByPlayer?(idxBattler) + @battleAI.pbDefaultChooseEnemyCommand(idxBattler) + next + end + # Player chooses an action + actioned.push(idxBattler) + commandsEnd = false # Whether to cancel choosing all other actions this round + loop do + cmd = pbCommandMenu(idxBattler,actioned.length==1) + # If being Sky Dropped, can't do anything except use a move + if cmd>0 && @battlers[idxBattler].effects[PBEffects::SkyDrop]>=0 + pbDisplay(_INTL("Sky Drop won't let {1} go!",@battlers[idxBattler].pbThis(true))) + next + end + case cmd + when 0 # Fight + break if pbFightMenu(idxBattler) + when 1 # Bag + if pbItemMenu(idxBattler,actioned.length==1) + commandsEnd = true if pbItemUsesAllActions?(@choices[idxBattler][1]) + break + end + when 2 # Pokémon + if pbPartyMenu(idxBattler) + @scene.setLastCommandIndex(idxBattler,0) + break + end + when 3 # Run + # NOTE: "Run" is only an available option for the first battler the + # player chooses an action for in a round. Attempting to run + # from battle prevents you from choosing any other actions in + # that round. + if pbRunMenu(idxBattler) + commandsEnd = true + break + end + when 4 # Call + break if pbCallMenu(idxBattler) + when -2 # Debug + pbDebugMenu + next + when -1 # Go back to previous battler's action choice + next if actioned.length<=1 + actioned.pop # Forget this battler was done + idxBattler = actioned.last-1 + pbCancelChoice(idxBattler+1) # Clear the previous battler's choice + actioned.pop # Forget the previous battler was done + break + end + pbCancelChoice(idxBattler) + end + break if commandsEnd + end + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/011_Battle_Phase_Attack.rb b/Data/Scripts/011_Battle/003_Battle/011_Battle_Phase_Attack.rb new file mode 100644 index 000000000..07bcaf913 --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/011_Battle_Phase_Attack.rb @@ -0,0 +1,190 @@ +class PokeBattle_Battle + #============================================================================= + # Attack phase actions + #============================================================================= + # Quick Claw, Custap Berry's "X let it move first!" message. + def pbAttackPhasePriorityChangeMessages + pbPriority.each do |b| + if b.effects[PBEffects::PriorityAbility] && b.abilityActive? + BattleHandlers.triggerPriorityBracketUseAbility(b.ability,b,self) + elsif b.effects[PBEffects::PriorityItem] && b.itemActive? + BattleHandlers.triggerPriorityBracketUseItem(b.item,b,self) + end + end + end + + def pbAttackPhaseCall + pbPriority.each do |b| + next unless @choices[b.index][0]==:Call && !b.fainted? + b.lastMoveFailed = false # Counts as a successful move for Stomping Tantrum + pbCall(b.index) + end + end + + def pbPursuit(idxSwitcher) + @switching = true + pbPriority.each do |b| + next if b.fainted? || !b.opposes?(idxSwitcher) # Shouldn't hit an ally + next if b.movedThisRound? || !pbChoseMoveFunctionCode?(b.index,"088") # Pursuit + # Check whether Pursuit can be used + next unless pbMoveCanTarget?(b.index,idxSwitcher,@choices[b.index][2].pbTarget(b)) + next unless pbCanChooseMove?(b.index,@choices[b.index][1],false) + next if b.status == :SLEEP || b.status == :FROZEN + next if b.effects[PBEffects::SkyDrop]>=0 + next if b.hasActiveAbility?(:TRUANT) && b.effects[PBEffects::Truant] + # Mega Evolve + if !wildBattle? || !b.opposes? + owner = pbGetOwnerIndexFromBattlerIndex(b.index) + pbMegaEvolve(b.index) if @megaEvolution[b.idxOwnSide][owner]==b.index + end + # Use Pursuit + @choices[b.index][3] = idxSwitcher # Change Pursuit's target + if b.pbProcessTurn(@choices[b.index],false) + b.effects[PBEffects::Pursuit] = true + end + break if @decision>0 || @battlers[idxSwitcher].fainted? + end + @switching = false + end + + def pbAttackPhaseSwitch + pbPriority.each do |b| + next unless @choices[b.index][0]==:SwitchOut && !b.fainted? + idxNewPkmn = @choices[b.index][1] # Party index of Pokémon to switch to + b.lastMoveFailed = false # Counts as a successful move for Stomping Tantrum + @lastMoveUser = b.index + # Switching message + pbMessageOnRecall(b) + # Pursuit interrupts switching + pbPursuit(b.index) + return if @decision>0 + # Switch Pokémon + pbRecallAndReplace(b.index,idxNewPkmn) + b.pbEffectsOnSwitchIn(true) + end + end + + def pbAttackPhaseItems + pbPriority.each do |b| + next unless @choices[b.index][0] == :UseItem && !b.fainted? + b.lastMoveFailed = false # Counts as a successful move for Stomping Tantrum + item = @choices[b.index][1] + next if !item + case GameData::Item.get(item).battle_use + when 1, 2, 6, 7 # Use on Pokémon/Pokémon's move + pbUseItemOnPokemon(item, @choices[b.index][2], b) if @choices[b.index][2] >= 0 + when 3, 8 # Use on battler + pbUseItemOnBattler(item, @choices[b.index][2], b) + when 4, 9 # Use Poké Ball + pbUsePokeBallInBattle(item, @choices[b.index][2], b) + when 5, 10 # Use directly + pbUseItemInBattle(item, @choices[b.index][2], b) + else + next + end + return if @decision > 0 + end + pbCalculatePriority if Settings::RECALCULATE_TURN_ORDER_AFTER_SPEED_CHANGES + end + + def pbAttackPhaseMegaEvolution + pbPriority.each do |b| + next if wildBattle? && b.opposes? + next unless @choices[b.index][0]==:UseMove && !b.fainted? + owner = pbGetOwnerIndexFromBattlerIndex(b.index) + next if @megaEvolution[b.idxOwnSide][owner]!=b.index + pbMegaEvolve(b.index) + end + end + + def pbAttackPhaseMoves + # Show charging messages (Focus Punch) + pbPriority.each do |b| + next unless @choices[b.index][0]==:UseMove && !b.fainted? + next if b.movedThisRound? + @choices[b.index][2].pbDisplayChargeMessage(b) + end + # Main move processing loop + loop do + priority = pbPriority + # Forced to go next + advance = false + priority.each do |b| + next unless b.effects[PBEffects::MoveNext] && !b.fainted? + next unless @choices[b.index][0]==:UseMove || @choices[b.index][0]==:Shift + next if b.movedThisRound? + advance = b.pbProcessTurn(@choices[b.index]) + break if advance + end + return if @decision>0 + next if advance + # Regular priority order + priority.each do |b| + next if b.effects[PBEffects::Quash]>0 || b.fainted? + next unless @choices[b.index][0]==:UseMove || @choices[b.index][0]==:Shift + next if b.movedThisRound? + advance = b.pbProcessTurn(@choices[b.index]) + break if advance + end + return if @decision>0 + next if advance + # Quashed + quashLevel = 0 + loop do + quashLevel += 1 + moreQuash = false + priority.each do |b| + moreQuash = true if b.effects[PBEffects::Quash]>quashLevel + next unless b.effects[PBEffects::Quash]==quashLevel && !b.fainted? + next unless @choices[b.index][0]==:UseMove || @choices[b.index][0]==:Shift + next if b.movedThisRound? + advance = b.pbProcessTurn(@choices[b.index]) + break + end + break if advance || !moreQuash + end + return if @decision>0 + next if advance + # Check for all done + priority.each do |b| + if !b.fainted? && !b.movedThisRound? + advance = true if @choices[b.index][0]==:UseMove || @choices[b.index][0]==:Shift + end + break if advance + end + next if advance + # All Pokémon have moved; end the loop + break + end + end + + #============================================================================= + # Attack phase + #============================================================================= + def pbAttackPhase + @scene.pbBeginAttackPhase + # Reset certain effects + @battlers.each_with_index do |b,i| + next if !b + b.turnCount += 1 if !b.fainted? + @successStates[i].clear + if @choices[i][0]!=:UseMove && @choices[i][0]!=:Shift && @choices[i][0]!=:SwitchOut + b.effects[PBEffects::DestinyBond] = false + b.effects[PBEffects::Grudge] = false + end + b.effects[PBEffects::Rage] = false if !pbChoseMoveFunctionCode?(i,"093") # Rage + end + PBDebug.log("") + # Calculate move order for this round + pbCalculatePriority(true) + # Perform actions + pbAttackPhasePriorityChangeMessages + pbAttackPhaseCall + pbAttackPhaseSwitch + return if @decision>0 + pbAttackPhaseItems + return if @decision>0 + pbAttackPhaseMegaEvolution + pbAttackPhaseMoves + end +end diff --git a/Data/Scripts/011_Battle/003_Battle/012_Battle_Phase_EndOfRound.rb b/Data/Scripts/011_Battle/003_Battle/012_Battle_Phase_EndOfRound.rb new file mode 100644 index 000000000..ab90bcf9e --- /dev/null +++ b/Data/Scripts/011_Battle/003_Battle/012_Battle_Phase_EndOfRound.rb @@ -0,0 +1,666 @@ +class PokeBattle_Battle + #============================================================================= + # Decrement effect counters + #============================================================================= + def pbEORCountDownBattlerEffect(priority,effect) + priority.each do |b| + next if b.fainted? || b.effects[effect]==0 + b.effects[effect] -= 1 + yield b if block_given? && b.effects[effect]==0 + end + end + + def pbEORCountDownSideEffect(side,effect,msg) + if @sides[side].effects[effect]>0 + @sides[side].effects[effect] -= 1 + pbDisplay(msg) if @sides[side].effects[effect]==0 + end + end + + def pbEORCountDownFieldEffect(effect,msg) + if @field.effects[effect]>0 + @field.effects[effect] -= 1 + if @field.effects[effect]==0 + pbDisplay(msg) + if effect==PBEffects::MagicRoom + pbPriority(true).each { |b| b.pbItemTerrainStatBoostCheck } + end + end + end + end + + #============================================================================= + # End Of Round weather + #============================================================================= + def pbEORWeather(priority) + # NOTE: Primordial weather doesn't need to be checked here, because if it + # could wear off here, it will have worn off already. + # Count down weather duration + @field.weatherDuration -= 1 if @field.weatherDuration>0 + # Weather wears off + if @field.weatherDuration==0 + case @field.weather + when :Sun then pbDisplay(_INTL("The sunlight faded.")) + when :Rain then pbDisplay(_INTL("The rain stopped.")) + when :Sandstorm then pbDisplay(_INTL("The sandstorm subsided.")) + when :Hail then pbDisplay(_INTL("The hail stopped.")) + when :ShadowSky then pbDisplay(_INTL("The shadow sky faded.")) + end + @field.weather = :None + # Check for form changes caused by the weather changing + eachBattler { |b| b.pbCheckFormOnWeatherChange } + # Start up the default weather + pbStartWeather(nil,@field.defaultWeather) if @field.defaultWeather != :None + return if @field.weather == :None + end + # Weather continues + weather_data = GameData::BattleWeather.try_get(@field.weather) + pbCommonAnimation(weather_data.animation) if weather_data + case @field.weather +# when :Sun then pbDisplay(_INTL("The sunlight is strong.")) +# when :Rain then pbDisplay(_INTL("Rain continues to fall.")) + when :Sandstorm then pbDisplay(_INTL("The sandstorm is raging.")) + when :Hail then pbDisplay(_INTL("The hail is crashing down.")) +# when :HarshSun then pbDisplay(_INTL("The sunlight is extremely harsh.")) +# when :HeavyRain then pbDisplay(_INTL("It is raining heavily.")) +# when :StrongWinds then pbDisplay(_INTL("The wind is strong.")) + when :ShadowSky then pbDisplay(_INTL("The shadow sky continues.")) + end + # Effects due to weather + curWeather = pbWeather + priority.each do |b| + # Weather-related abilities + if b.abilityActive? + BattleHandlers.triggerEORWeatherAbility(b.ability,curWeather,b,self) + b.pbFaint if b.fainted? + end + # Weather damage + # NOTE: + case curWeather + when :Sandstorm + next if !b.takesSandstormDamage? + pbDisplay(_INTL("{1} is buffeted by the sandstorm!",b.pbThis)) + @scene.pbDamageAnimation(b) + b.pbReduceHP(b.totalhp/16,false) + b.pbItemHPHealCheck + b.pbFaint if b.fainted? + when :Hail + next if !b.takesHailDamage? + pbDisplay(_INTL("{1} is buffeted by the hail!",b.pbThis)) + @scene.pbDamageAnimation(b) + b.pbReduceHP(b.totalhp/16,false) + b.pbItemHPHealCheck + b.pbFaint if b.fainted? + when :ShadowSky + next if !b.takesShadowSkyDamage? + pbDisplay(_INTL("{1} is hurt by the shadow sky!",b.pbThis)) + @scene.pbDamageAnimation(b) + b.pbReduceHP(b.totalhp/16,false) + b.pbItemHPHealCheck + b.pbFaint if b.fainted? + end + end + end + + #============================================================================= + # End Of Round terrain + #============================================================================= + def pbEORTerrain + # Count down terrain duration + @field.terrainDuration -= 1 if @field.terrainDuration>0 + # Terrain wears off + if @field.terrain != :None && @field.terrainDuration == 0 + case @field.terrain + when :Electric + pbDisplay(_INTL("The electric current disappeared from the battlefield!")) + when :Grassy + pbDisplay(_INTL("The grass disappeared from the battlefield!")) + when :Misty + pbDisplay(_INTL("The mist disappeared from the battlefield!")) + when :Psychic + pbDisplay(_INTL("The weirdness disappeared from the battlefield!")) + end + @field.terrain = :None + # Start up the default terrain + pbStartTerrain(nil, @field.defaultTerrain, false) if @field.defaultTerrain != :None + return if @field.terrain == :None + end + # Terrain continues + terrain_data = GameData::BattleTerrain.try_get(@field.terrain) + pbCommonAnimation(terrain_data.animation) if terrain_data + case @field.terrain + when :Electric then pbDisplay(_INTL("An electric current is running across the battlefield.")) + when :Grassy then pbDisplay(_INTL("Grass is covering the battlefield.")) + when :Misty then pbDisplay(_INTL("Mist is swirling about the battlefield.")) + when :Psychic then pbDisplay(_INTL("The battlefield is weird.")) + end + end + + #============================================================================= + # End Of Round shift distant battlers to middle positions + #============================================================================= + def pbEORShiftDistantBattlers + # Move battlers around if none are near to each other + # NOTE: This code assumes each side has a maximum of 3 battlers on it, and + # is not generalised to larger side sizes. + if !singleBattle? + swaps = [] # Each element is an array of two battler indices to swap + for side in 0...2 + next if pbSideSize(side)==1 # Only battlers on sides of size 2+ need to move + # Check if any battler on this side is near any battler on the other side + anyNear = false + eachSameSideBattler(side) do |b| + eachOtherSideBattler(b) do |otherB| + next if !nearBattlers?(otherB.index,b.index) + anyNear = true + break + end + break if anyNear + end + break if anyNear + # No battlers on this side are near any battlers on the other side; try + # to move them + # NOTE: If we get to here (assuming both sides are of size 3 or less), + # there is definitely only 1 able battler on this side, so we + # don't need to worry about multiple battlers trying to move into + # the same position. If you add support for a side of size 4+, + # this code will need revising to account for that, as well as to + # add more complex code to ensure battlers will end up near each + # other. + eachSameSideBattler(side) do |b| + # Get the position to move to + pos = -1 + case pbSideSize(side) + when 2 then pos = [2,3,0,1][b.index] # The unoccupied position + when 3 then pos = (side==0) ? 2 : 3 # The centre position + end + next if pos<0 + # Can't move if the same trainer doesn't control both positions + idxOwner = pbGetOwnerIndexFromBattlerIndex(b.index) + next if pbGetOwnerIndexFromBattlerIndex(pos)!=idxOwner + swaps.push([b.index,pos]) + end + end + # Move battlers around + swaps.each do |pair| + next if pbSideSize(pair[0])==2 && swaps.length>1 + next if !pbSwapBattlers(pair[0],pair[1]) + case pbSideSize(side) + when 2 + pbDisplay(_INTL("{1} moved across!",@battlers[pair[1]].pbThis)) + when 3 + pbDisplay(_INTL("{1} moved to the center!",@battlers[pair[1]].pbThis)) + end + end + end + end + + #============================================================================= + # End Of Round phase + #============================================================================= + def pbEndOfRoundPhase + PBDebug.log("") + PBDebug.log("[End of round]") + @endOfRound = true + @scene.pbBeginEndOfRoundPhase + pbCalculatePriority # recalculate speeds + priority = pbPriority(true) # in order of fastest -> slowest speeds only + # Weather + pbEORWeather(priority) + # Future Sight/Doom Desire + @positions.each_with_index do |pos,idxPos| + next if !pos || pos.effects[PBEffects::FutureSightCounter]==0 + pos.effects[PBEffects::FutureSightCounter] -= 1 + next if pos.effects[PBEffects::FutureSightCounter]>0 + next if !@battlers[idxPos] || @battlers[idxPos].fainted? # No target + moveUser = nil + eachBattler do |b| + next if b.opposes?(pos.effects[PBEffects::FutureSightUserIndex]) + next if b.pokemonIndex!=pos.effects[PBEffects::FutureSightUserPartyIndex] + moveUser = b + break + end + next if moveUser && moveUser.index==idxPos # Target is the user + if !moveUser # User isn't in battle, get it from the party + party = pbParty(pos.effects[PBEffects::FutureSightUserIndex]) + pkmn = party[pos.effects[PBEffects::FutureSightUserPartyIndex]] + if pkmn && pkmn.able? + moveUser = PokeBattle_Battler.new(self,pos.effects[PBEffects::FutureSightUserIndex]) + moveUser.pbInitDummyPokemon(pkmn,pos.effects[PBEffects::FutureSightUserPartyIndex]) + end + end + next if !moveUser # User is fainted + move = pos.effects[PBEffects::FutureSightMove] + pbDisplay(_INTL("{1} took the {2} attack!",@battlers[idxPos].pbThis, + GameData::Move.get(move).name)) + # NOTE: Future Sight failing against the target here doesn't count towards + # Stomping Tantrum. + userLastMoveFailed = moveUser.lastMoveFailed + @futureSight = true + moveUser.pbUseMoveSimple(move,idxPos) + @futureSight = false + moveUser.lastMoveFailed = userLastMoveFailed + @battlers[idxPos].pbFaint if @battlers[idxPos].fainted? + pos.effects[PBEffects::FutureSightCounter] = 0 + pos.effects[PBEffects::FutureSightMove] = nil + pos.effects[PBEffects::FutureSightUserIndex] = -1 + pos.effects[PBEffects::FutureSightUserPartyIndex] = -1 + end + # Wish + @positions.each_with_index do |pos,idxPos| + next if !pos || pos.effects[PBEffects::Wish]==0 + pos.effects[PBEffects::Wish] -= 1 + next if pos.effects[PBEffects::Wish]>0 + next if !@battlers[idxPos] || !@battlers[idxPos].canHeal? + wishMaker = pbThisEx(idxPos,pos.effects[PBEffects::WishMaker]) + @battlers[idxPos].pbRecoverHP(pos.effects[PBEffects::WishAmount]) + pbDisplay(_INTL("{1}'s wish came true!",wishMaker)) + end + # Sea of Fire damage (Fire Pledge + Grass Pledge combination) + curWeather = pbWeather + for side in 0...2 + next if sides[side].effects[PBEffects::SeaOfFire]==0 + next if [:Rain, :HeavyRain].include?(curWeather) + @battle.pbCommonAnimation("SeaOfFire") if side==0 + @battle.pbCommonAnimation("SeaOfFireOpp") if side==1 + priority.each do |b| + next if b.opposes?(side) + next if !b.takesIndirectDamage? || b.pbHasType?(:FIRE) + oldHP = b.hp + @scene.pbDamageAnimation(b) + b.pbReduceHP(b.totalhp/8,false) + pbDisplay(_INTL("{1} is hurt by the sea of fire!",b.pbThis)) + b.pbItemHPHealCheck + b.pbAbilitiesOnDamageTaken(oldHP) + b.pbFaint if b.fainted? + end + end + # Status-curing effects/abilities and HP-healing items + priority.each do |b| + next if b.fainted? + # Grassy Terrain (healing) + if @field.terrain == :Grassy && b.affectedByTerrain? && b.canHeal? + PBDebug.log("[Lingering effect] Grassy Terrain heals #{b.pbThis(true)}") + b.pbRecoverHP(b.totalhp/16) + pbDisplay(_INTL("{1}'s HP was restored.",b.pbThis)) + end + # Healer, Hydration, Shed Skin + BattleHandlers.triggerEORHealingAbility(b.ability,b,self) if b.abilityActive? + # Black Sludge, Leftovers + BattleHandlers.triggerEORHealingItem(b.item,b,self) if b.itemActive? + end + # Aqua Ring + priority.each do |b| + next if !b.effects[PBEffects::AquaRing] + next if !b.canHeal? + hpGain = b.totalhp/16 + hpGain = (hpGain*1.3).floor if b.hasActiveItem?(:BIGROOT) + b.pbRecoverHP(hpGain) + pbDisplay(_INTL("Aqua Ring restored {1}'s HP!",b.pbThis(true))) + end + # Ingrain + priority.each do |b| + next if !b.effects[PBEffects::Ingrain] + next if !b.canHeal? + hpGain = b.totalhp/16 + hpGain = (hpGain*1.3).floor if b.hasActiveItem?(:BIGROOT) + b.pbRecoverHP(hpGain) + pbDisplay(_INTL("{1} absorbed nutrients with its roots!",b.pbThis)) + end + # Leech Seed + priority.each do |b| + next if b.effects[PBEffects::LeechSeed]<0 + next if !b.takesIndirectDamage? + recipient = @battlers[b.effects[PBEffects::LeechSeed]] + next if !recipient || recipient.fainted? + oldHP = b.hp + oldHPRecipient = recipient.hp + pbCommonAnimation("LeechSeed",recipient,b) + hpLoss = b.pbReduceHP(b.totalhp/8) + recipient.pbRecoverHPFromDrain(hpLoss,b, + _INTL("{1}'s health is sapped by Leech Seed!",b.pbThis)) + recipient.pbAbilitiesOnDamageTaken(oldHPRecipient) if recipient.hp0 + b.effects[PBEffects::Toxic] += 1 + b.effects[PBEffects::Toxic] = 15 if b.effects[PBEffects::Toxic]>15 + end + if b.hasActiveAbility?(:POISONHEAL) + if b.canHeal? + anim_name = GameData::Status.get(:POISON).animation + pbCommonAnimation(anim_name, b) if anim_name + pbShowAbilitySplash(b) + b.pbRecoverHP(b.totalhp/8) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + pbDisplay(_INTL("{1}'s HP was restored.",b.pbThis)) + else + pbDisplay(_INTL("{1}'s {2} restored its HP.",b.pbThis,b.abilityName)) + end + pbHideAbilitySplash(b) + end + elsif b.takesIndirectDamage? + oldHP = b.hp + dmg = (b.statusCount==0) ? b.totalhp/8 : b.totalhp*b.effects[PBEffects::Toxic]/16 + b.pbContinueStatus { b.pbReduceHP(dmg,false) } + b.pbItemHPHealCheck + b.pbAbilitiesOnDamageTaken(oldHP) + b.pbFaint if b.fainted? + end + end + # Damage from burn + priority.each do |b| + next if b.status != :BURN || !b.takesIndirectDamage? + oldHP = b.hp + dmg = (Settings::MECHANICS_GENERATION >= 7) ? b.totalhp/16 : b.totalhp/8 + dmg = (dmg/2.0).round if b.hasActiveAbility?(:HEATPROOF) + b.pbContinueStatus { b.pbReduceHP(dmg,false) } + b.pbItemHPHealCheck + b.pbAbilitiesOnDamageTaken(oldHP) + b.pbFaint if b.fainted? + end + # Damage from sleep (Nightmare) + priority.each do |b| + b.effects[PBEffects::Nightmare] = false if !b.asleep? + next if !b.effects[PBEffects::Nightmare] || !b.takesIndirectDamage? + oldHP = b.hp + b.pbReduceHP(b.totalhp/4) + pbDisplay(_INTL("{1} is locked in a nightmare!",b.pbThis)) + b.pbItemHPHealCheck + b.pbAbilitiesOnDamageTaken(oldHP) + b.pbFaint if b.fainted? + end + # Curse + priority.each do |b| + next if !b.effects[PBEffects::Curse] || !b.takesIndirectDamage? + oldHP = b.hp + b.pbReduceHP(b.totalhp/4) + pbDisplay(_INTL("{1} is afflicted by the curse!",b.pbThis)) + b.pbItemHPHealCheck + b.pbAbilitiesOnDamageTaken(oldHP) + b.pbFaint if b.fainted? + end + # Trapping attacks (Bind/Clamp/Fire Spin/Magma Storm/Sand Tomb/Whirlpool/Wrap) + priority.each do |b| + next if b.fainted? || b.effects[PBEffects::Trapping]==0 + b.effects[PBEffects::Trapping] -= 1 + moveName = GameData::Move.get(b.effects[PBEffects::TrappingMove]).name + if b.effects[PBEffects::Trapping]==0 + pbDisplay(_INTL("{1} was freed from {2}!",b.pbThis,moveName)) + else + case b.effects[PBEffects::TrappingMove] + when :BIND then pbCommonAnimation("Bind", b) + when :CLAMP then pbCommonAnimation("Clamp", b) + when :FIRESPIN then pbCommonAnimation("FireSpin", b) + when :MAGMASTORM then pbCommonAnimation("MagmaStorm", b) + when :SANDTOMB then pbCommonAnimation("SandTomb", b) + when :WRAP then pbCommonAnimation("Wrap", b) + when :INFESTATION then pbCommonAnimation("Infestation", b) + else pbCommonAnimation("Wrap", b) + end + if b.takesIndirectDamage? + hpLoss = (Settings::MECHANICS_GENERATION >= 6) ? b.totalhp/8 : b.totalhp/16 + if @battlers[b.effects[PBEffects::TrappingUser]].hasActiveItem?(:BINDINGBAND) + hpLoss = (Settings::MECHANICS_GENERATION >= 6) ? b.totalhp/6 : b.totalhp/8 + end + @scene.pbDamageAnimation(b) + b.pbReduceHP(hpLoss,false) + pbDisplay(_INTL("{1} is hurt by {2}!",b.pbThis,moveName)) + b.pbItemHPHealCheck + # NOTE: No need to call pbAbilitiesOnDamageTaken as b can't switch out. + b.pbFaint if b.fainted? + end + end + end + # Taunt + pbEORCountDownBattlerEffect(priority,PBEffects::Taunt) { |battler| + pbDisplay(_INTL("{1}'s taunt wore off!",battler.pbThis)) + } + # Encore + priority.each do |b| + next if b.fainted? || b.effects[PBEffects::Encore]==0 + idxEncoreMove = b.pbEncoredMoveIndex + if idxEncoreMove>=0 + b.effects[PBEffects::Encore] -= 1 + if b.effects[PBEffects::Encore]==0 || b.moves[idxEncoreMove].pp==0 + b.effects[PBEffects::Encore] = 0 + pbDisplay(_INTL("{1}'s encore ended!",b.pbThis)) + end + else + PBDebug.log("[End of effect] #{b.pbThis}'s encore ended (encored move no longer known)") + b.effects[PBEffects::Encore] = 0 + b.effects[PBEffects::EncoreMove] = nil + end + end + # Disable/Cursed Body + pbEORCountDownBattlerEffect(priority,PBEffects::Disable) { |battler| + battler.effects[PBEffects::DisableMove] = nil + pbDisplay(_INTL("{1} is no longer disabled!",battler.pbThis)) + } + # Magnet Rise + pbEORCountDownBattlerEffect(priority,PBEffects::MagnetRise) { |battler| + pbDisplay(_INTL("{1}'s electromagnetism wore off!",battler.pbThis)) + } + # Telekinesis + pbEORCountDownBattlerEffect(priority,PBEffects::Telekinesis) { |battler| + pbDisplay(_INTL("{1} was freed from the telekinesis!",battler.pbThis)) + } + # Heal Block + pbEORCountDownBattlerEffect(priority,PBEffects::HealBlock) { |battler| + pbDisplay(_INTL("{1}'s Heal Block wore off!",battler.pbThis)) + } + # Embargo + pbEORCountDownBattlerEffect(priority,PBEffects::Embargo) { |battler| + pbDisplay(_INTL("{1} can use items again!",battler.pbThis)) + battler.pbItemTerrainStatBoostCheck + } + # Yawn + pbEORCountDownBattlerEffect(priority,PBEffects::Yawn) { |battler| + if battler.pbCanSleepYawn? + PBDebug.log("[Lingering effect] #{battler.pbThis} fell asleep because of Yawn") + battler.pbSleep + end + } + # Perish Song + perishSongUsers = [] + priority.each do |b| + next if b.fainted? || b.effects[PBEffects::PerishSong]==0 + b.effects[PBEffects::PerishSong] -= 1 + pbDisplay(_INTL("{1}'s perish count fell to {2}!",b.pbThis,b.effects[PBEffects::PerishSong])) + if b.effects[PBEffects::PerishSong]==0 + perishSongUsers.push(b.effects[PBEffects::PerishSongUser]) + b.pbReduceHP(b.hp) + end + b.pbItemHPHealCheck + b.pbFaint if b.fainted? + end + if perishSongUsers.length>0 + # If all remaining Pokemon fainted by a Perish Song triggered by a single side + if (perishSongUsers.find_all { |idxBattler| opposes?(idxBattler) }.length==perishSongUsers.length) || + (perishSongUsers.find_all { |idxBattler| !opposes?(idxBattler) }.length==perishSongUsers.length) + pbJudgeCheckpoint(@battlers[perishSongUsers[0]]) + end + end + # Check for end of battle + if @decision>0 + pbGainExp + return + end + for side in 0...2 + # Reflect + pbEORCountDownSideEffect(side,PBEffects::Reflect, + _INTL("{1}'s Reflect wore off!",@battlers[side].pbTeam)) + # Light Screen + pbEORCountDownSideEffect(side,PBEffects::LightScreen, + _INTL("{1}'s Light Screen wore off!",@battlers[side].pbTeam)) + # Safeguard + pbEORCountDownSideEffect(side,PBEffects::Safeguard, + _INTL("{1} is no longer protected by Safeguard!",@battlers[side].pbTeam)) + # Mist + pbEORCountDownSideEffect(side,PBEffects::Mist, + _INTL("{1} is no longer protected by mist!",@battlers[side].pbTeam)) + # Tailwind + pbEORCountDownSideEffect(side,PBEffects::Tailwind, + _INTL("{1}'s Tailwind petered out!",@battlers[side].pbTeam)) + # Lucky Chant + pbEORCountDownSideEffect(side,PBEffects::LuckyChant, + _INTL("{1}'s Lucky Chant wore off!",@battlers[side].pbTeam)) + # Pledge Rainbow + pbEORCountDownSideEffect(side,PBEffects::Rainbow, + _INTL("The rainbow on {1}'s side disappeared!",@battlers[side].pbTeam(true))) + # Pledge Sea of Fire + pbEORCountDownSideEffect(side,PBEffects::SeaOfFire, + _INTL("The sea of fire around {1} disappeared!",@battlers[side].pbTeam(true))) + # Pledge Swamp + pbEORCountDownSideEffect(side,PBEffects::Swamp, + _INTL("The swamp around {1} disappeared!",@battlers[side].pbTeam(true))) + # Aurora Veil + pbEORCountDownSideEffect(side,PBEffects::AuroraVeil, + _INTL("{1}'s Aurora Veil wore off!",@battlers[side].pbTeam(true))) + end + # Trick Room + pbEORCountDownFieldEffect(PBEffects::TrickRoom, + _INTL("The twisted dimensions returned to normal!")) + # Gravity + pbEORCountDownFieldEffect(PBEffects::Gravity, + _INTL("Gravity returned to normal!")) + # Water Sport + pbEORCountDownFieldEffect(PBEffects::WaterSportField, + _INTL("The effects of Water Sport have faded.")) + # Mud Sport + pbEORCountDownFieldEffect(PBEffects::MudSportField, + _INTL("The effects of Mud Sport have faded.")) + # Wonder Room + pbEORCountDownFieldEffect(PBEffects::WonderRoom, + _INTL("Wonder Room wore off, and Defense and Sp. Def stats returned to normal!")) + # Magic Room + pbEORCountDownFieldEffect(PBEffects::MagicRoom, + _INTL("Magic Room wore off, and held items' effects returned to normal!")) + # End of terrains + pbEORTerrain + priority.each do |b| + next if b.fainted? + # Hyper Mode (Shadow Pokémon) + if b.inHyperMode? + if pbRandom(100)<10 + b.pokemon.hyper_mode = false + b.pokemon.adjustHeart(-50) + pbDisplay(_INTL("{1} came to its senses!",b.pbThis)) + else + pbDisplay(_INTL("{1} is in Hyper Mode!",b.pbThis)) + end + end + # Uproar + if b.effects[PBEffects::Uproar]>0 + b.effects[PBEffects::Uproar] -= 1 + if b.effects[PBEffects::Uproar]==0 + pbDisplay(_INTL("{1} calmed down.",b.pbThis)) + else + pbDisplay(_INTL("{1} is making an uproar!",b.pbThis)) + end + end + # Slow Start's end message + if b.effects[PBEffects::SlowStart]>0 + b.effects[PBEffects::SlowStart] -= 1 + if b.effects[PBEffects::SlowStart]==0 + pbDisplay(_INTL("{1} finally got its act together!",b.pbThis)) + end + end + # Bad Dreams, Moody, Speed Boost + BattleHandlers.triggerEOREffectAbility(b.ability,b,self) if b.abilityActive? + # Flame Orb, Sticky Barb, Toxic Orb + BattleHandlers.triggerEOREffectItem(b.item,b,self) if b.itemActive? + # Harvest, Pickup + BattleHandlers.triggerEORGainItemAbility(b.ability,b,self) if b.abilityActive? + end + pbGainExp + return if @decision>0 + # Form checks + priority.each { |b| b.pbCheckForm(true) } + # Switch Pokémon in if possible + pbEORSwitch + return if @decision>0 + # In battles with at least one side of size 3+, move battlers around if none + # are near to any foes + pbEORShiftDistantBattlers + # Try to make Trace work, check for end of primordial weather + priority.each { |b| b.pbContinualAbilityChecks } + # Reset/count down battler-specific effects (no messages) + eachBattler do |b| + b.effects[PBEffects::BanefulBunker] = false + b.effects[PBEffects::Charge] -= 1 if b.effects[PBEffects::Charge]>0 + b.effects[PBEffects::Counter] = -1 + b.effects[PBEffects::CounterTarget] = -1 + b.effects[PBEffects::Electrify] = false + b.effects[PBEffects::Endure] = false + b.effects[PBEffects::FirstPledge] = 0 + b.effects[PBEffects::Flinch] = false + b.effects[PBEffects::FocusPunch] = false + b.effects[PBEffects::FollowMe] = 0 + b.effects[PBEffects::HelpingHand] = false + b.effects[PBEffects::HyperBeam] -= 1 if b.effects[PBEffects::HyperBeam]>0 + b.effects[PBEffects::KingsShield] = false + b.effects[PBEffects::LaserFocus] -= 1 if b.effects[PBEffects::LaserFocus]>0 + if b.effects[PBEffects::LockOn]>0 # Also Mind Reader + b.effects[PBEffects::LockOn] -= 1 + b.effects[PBEffects::LockOnPos] = -1 if b.effects[PBEffects::LockOn]==0 + end + b.effects[PBEffects::MagicBounce] = false + b.effects[PBEffects::MagicCoat] = false + b.effects[PBEffects::MirrorCoat] = -1 + b.effects[PBEffects::MirrorCoatTarget] = -1 + b.effects[PBEffects::Powder] = false + b.effects[PBEffects::Prankster] = false + b.effects[PBEffects::PriorityAbility] = false + b.effects[PBEffects::PriorityItem] = false + b.effects[PBEffects::Protect] = false + b.effects[PBEffects::RagePowder] = false + b.effects[PBEffects::Roost] = false + b.effects[PBEffects::Snatch] = 0 + b.effects[PBEffects::SpikyShield] = false + b.effects[PBEffects::Spotlight] = 0 + b.effects[PBEffects::ThroatChop] -= 1 if b.effects[PBEffects::ThroatChop]>0 + b.lastHPLost = 0 + b.lastHPLostFromFoe = 0 + b.tookDamage = false + b.tookPhysicalHit = false + b.lastRoundMoveFailed = b.lastMoveFailed + b.lastAttacker.clear + b.lastFoeAttacker.clear + end + # Reset/count down side-specific effects (no messages) + for side in 0...2 + @sides[side].effects[PBEffects::CraftyShield] = false + if !@sides[side].effects[PBEffects::EchoedVoiceUsed] + @sides[side].effects[PBEffects::EchoedVoiceCounter] = 0 + end + @sides[side].effects[PBEffects::EchoedVoiceUsed] = false + @sides[side].effects[PBEffects::MatBlock] = false + @sides[side].effects[PBEffects::QuickGuard] = false + @sides[side].effects[PBEffects::Round] = false + @sides[side].effects[PBEffects::WideGuard] = false + end + # Reset/count down field-specific effects (no messages) + @field.effects[PBEffects::IonDeluge] = false + @field.effects[PBEffects::FairyLock] -= 1 if @field.effects[PBEffects::FairyLock]>0 + @field.effects[PBEffects::FusionBolt] = false + @field.effects[PBEffects::FusionFlare] = false + @endOfRound = false + end +end diff --git a/Data/Scripts/011_Battle/003_BattleHandlers_Abilities.rb b/Data/Scripts/011_Battle/003_BattleHandlers_Abilities.rb new file mode 100644 index 000000000..0d76a08fa --- /dev/null +++ b/Data/Scripts/011_Battle/003_BattleHandlers_Abilities.rb @@ -0,0 +1,2426 @@ +#=============================================================================== +# SpeedCalcAbility handlers +#=============================================================================== + +BattleHandlers::SpeedCalcAbility.add(:CHLOROPHYLL, + proc { |ability,battler,mult| + next mult * 2 if [:Sun, :HarshSun].include?(battler.battle.pbWeather) + } +) + +BattleHandlers::SpeedCalcAbility.add(:QUICKFEET, + proc { |ability,battler,mult| + next mult*1.5 if battler.pbHasAnyStatus? + } +) + +BattleHandlers::SpeedCalcAbility.add(:SANDRUSH, + proc { |ability,battler,mult| + next mult * 2 if [:Sandstorm].include?(battler.battle.pbWeather) + } +) + +BattleHandlers::SpeedCalcAbility.add(:SLOWSTART, + proc { |ability,battler,mult| + next mult/2 if battler.effects[PBEffects::SlowStart]>0 + } +) + +BattleHandlers::SpeedCalcAbility.add(:SLUSHRUSH, + proc { |ability,battler,mult| + next mult * 2 if [:Hail].include?(battler.battle.pbWeather) + } +) + +BattleHandlers::SpeedCalcAbility.add(:SURGESURFER, + proc { |ability,battler,mult| + next mult*2 if battler.battle.field.terrain == :Electric + } +) + +BattleHandlers::SpeedCalcAbility.add(:SWIFTSWIM, + proc { |ability,battler,mult| + next mult * 2 if [:Rain, :HeavyRain].include?(battler.battle.pbWeather) + } +) + +BattleHandlers::SpeedCalcAbility.add(:UNBURDEN, + proc { |ability,battler,mult| + next mult*2 if battler.effects[PBEffects::Unburden] && !battler.item + } +) + +#=============================================================================== +# WeightCalcAbility handlers +#=============================================================================== + +BattleHandlers::WeightCalcAbility.add(:HEAVYMETAL, + proc { |ability,battler,w| + next w*2 + } +) + +BattleHandlers::WeightCalcAbility.add(:LIGHTMETAL, + proc { |ability,battler,w| + next [w/2,1].max + } +) + +#=============================================================================== +# AbilityOnHPDroppedBelowHalf handlers +#=============================================================================== + +BattleHandlers::AbilityOnHPDroppedBelowHalf.add(:EMERGENCYEXIT, + proc { |ability,battler,battle| + next false if battler.effects[PBEffects::SkyDrop]>=0 || battler.inTwoTurnAttack?("0CE") # Sky Drop + # In wild battles + if battle.wildBattle? + next false if battler.opposes? && battle.pbSideBattlerCount(battler.index)>1 + next false if !battle.pbCanRun?(battler.index) + battle.pbShowAbilitySplash(battler,true) + battle.pbHideAbilitySplash(battler) + pbSEPlay("Battle flee") + battle.pbDisplay(_INTL("{1} fled from battle!",battler.pbThis)) + battle.decision = 3 # Escaped + next true + end + # In trainer battles + next false if battle.pbAllFainted?(battler.idxOpposingSide) + next false if !battle.pbCanSwitch?(battler.index) # Battler can't switch out + next false if !battle.pbCanChooseNonActive?(battler.index) # No Pokémon can switch in + battle.pbShowAbilitySplash(battler,true) + battle.pbHideAbilitySplash(battler) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s {2} activated!",battler.pbThis,battler.abilityName)) + end + battle.pbDisplay(_INTL("{1} went back to {2}!", + battler.pbThis,battle.pbGetOwnerName(battler.index))) + if battle.endOfRound # Just switch out + battle.scene.pbRecall(battler.index) if !battler.fainted? + battler.pbAbilitiesOnSwitchOut # Inc. primordial weather check + next true + end + newPkmn = battle.pbGetReplacementPokemonIndex(battler.index) # Owner chooses + next false if newPkmn<0 # Shouldn't ever do this + battle.pbRecallAndReplace(battler.index,newPkmn) + battle.pbClearChoice(battler.index) # Replacement Pokémon does nothing this round + next true + } +) + +BattleHandlers::AbilityOnHPDroppedBelowHalf.copy(:EMERGENCYEXIT,:WIMPOUT) + +#=============================================================================== +# StatusCheckAbilityNonIgnorable handlers +#=============================================================================== + +BattleHandlers::StatusCheckAbilityNonIgnorable.add(:COMATOSE, + proc { |ability,battler,status| + next false if !battler.isSpecies?(:KOMALA) + next true if status.nil? || status == :SLEEP + } +) + +#=============================================================================== +# StatusImmunityAbility handlers +#=============================================================================== + +BattleHandlers::StatusImmunityAbility.add(:FLOWERVEIL, + proc { |ability,battler,status| + next true if battler.pbHasType?(:GRASS) + } +) + +BattleHandlers::StatusImmunityAbility.add(:IMMUNITY, + proc { |ability,battler,status| + next true if status == :POISON + } +) + +BattleHandlers::StatusImmunityAbility.add(:INSOMNIA, + proc { |ability,battler,status| + next true if status == :SLEEP + } +) + +BattleHandlers::StatusImmunityAbility.copy(:INSOMNIA,:SWEETVEIL,:VITALSPIRIT) + +BattleHandlers::StatusImmunityAbility.add(:LEAFGUARD, + proc { |ability,battler,status| + next true if [:Sun, :HarshSun].include?(battler.battle.pbWeather) + } +) + +BattleHandlers::StatusImmunityAbility.add(:LIMBER, + proc { |ability,battler,status| + next true if status == :PARALYSIS + } +) + +BattleHandlers::StatusImmunityAbility.add(:MAGMAARMOR, + proc { |ability,battler,status| + next true if status == :FROZEN + } +) + +BattleHandlers::StatusImmunityAbility.add(:WATERVEIL, + proc { |ability,battler,status| + next true if status == :BURN + } +) + +BattleHandlers::StatusImmunityAbility.copy(:WATERVEIL,:WATERBUBBLE) + +#=============================================================================== +# StatusImmunityAbilityNonIgnorable handlers +#=============================================================================== + +BattleHandlers::StatusImmunityAbilityNonIgnorable.add(:COMATOSE, + proc { |ability,battler,status| + next true if battler.isSpecies?(:KOMALA) + } +) + +BattleHandlers::StatusImmunityAbilityNonIgnorable.add(:SHIELDSDOWN, + proc { |ability,battler,status| + next true if battler.isFusionOf(:MINIOR_C) && battler.form<7 + } +) + +#=============================================================================== +# StatusImmunityAllyAbility handlers +#=============================================================================== + +BattleHandlers::StatusImmunityAllyAbility.add(:FLOWERVEIL, + proc { |ability,battler,status| + next true if battler.pbHasType?(:GRASS) + } +) + +BattleHandlers::StatusImmunityAbility.add(:SWEETVEIL, + proc { |ability,battler,status| + next true if status == :SLEEP + } +) + +#=============================================================================== +# AbilityOnStatusInflicted handlers +#=============================================================================== + +BattleHandlers::AbilityOnStatusInflicted.add(:SYNCHRONIZE, + proc { |ability,battler,user,status| + next if !user || user.index==battler.index + case status + when :POISON + if user.pbCanPoisonSynchronize?(battler) + battler.battle.pbShowAbilitySplash(battler) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} poisoned {3}!",battler.pbThis,battler.abilityName,user.pbThis(true)) + end + user.pbPoison(nil,msg,(battler.statusCount>0)) + battler.battle.pbHideAbilitySplash(battler) + end + when :BURN + if user.pbCanBurnSynchronize?(battler) + battler.battle.pbShowAbilitySplash(battler) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} burned {3}!",battler.pbThis,battler.abilityName,user.pbThis(true)) + end + user.pbBurn(nil,msg) + battler.battle.pbHideAbilitySplash(battler) + end + when :PARALYSIS + if user.pbCanParalyzeSynchronize?(battler) + battler.battle.pbShowAbilitySplash(battler) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} paralyzed {3}! It may be unable to move!", + battler.pbThis,battler.abilityName,user.pbThis(true)) + end + user.pbParalyze(nil,msg) + battler.battle.pbHideAbilitySplash(battler) + end + end + } +) + +#=============================================================================== +# StatusCureAbility handlers +#=============================================================================== + +BattleHandlers::StatusCureAbility.add(:IMMUNITY, + proc { |ability,battler| + next if battler.status != :POISON + battler.battle.pbShowAbilitySplash(battler) + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1}'s {2} cured its poisoning!",battler.pbThis,battler.abilityName)) + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.add(:INSOMNIA, + proc { |ability,battler| + next if battler.status != :SLEEP + battler.battle.pbShowAbilitySplash(battler) + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1}'s {2} woke it up!",battler.pbThis,battler.abilityName)) + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.copy(:INSOMNIA,:VITALSPIRIT) + +BattleHandlers::StatusCureAbility.add(:LIMBER, + proc { |ability,battler| + next if battler.status != :PARALYSIS + battler.battle.pbShowAbilitySplash(battler) + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1}'s {2} cured its paralysis!",battler.pbThis,battler.abilityName)) + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.add(:MAGMAARMOR, + proc { |ability,battler| + next if battler.status != :FROZEN + battler.battle.pbShowAbilitySplash(battler) + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1}'s {2} defrosted it!",battler.pbThis,battler.abilityName)) + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.add(:OBLIVIOUS, + proc { |ability,battler| + next if battler.effects[PBEffects::Attract]<0 && + (battler.effects[PBEffects::Taunt]==0 || Settings::MECHANICS_GENERATION <= 5) + battler.battle.pbShowAbilitySplash(battler) + if battler.effects[PBEffects::Attract]>=0 + battler.pbCureAttract + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1} got over its infatuation.",battler.pbThis)) + else + battler.battle.pbDisplay(_INTL("{1}'s {2} cured its infatuation status!", + battler.pbThis,battler.abilityName)) + end + end + if battler.effects[PBEffects::Taunt]>0 && Settings::MECHANICS_GENERATION >= 6 + battler.effects[PBEffects::Taunt] = 0 + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1}'s Taunt wore off!",battler.pbThis)) + else + battler.battle.pbDisplay(_INTL("{1}'s {2} made its taunt wear off!", + battler.pbThis,battler.abilityName)) + end + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.add(:OWNTEMPO, + proc { |ability,battler| + next if battler.effects[PBEffects::Confusion]==0 + battler.battle.pbShowAbilitySplash(battler) + battler.pbCureConfusion + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1} snapped out of its confusion.",battler.pbThis)) + else + battler.battle.pbDisplay(_INTL("{1}'s {2} snapped it out of its confusion!", + battler.pbThis,battler.abilityName)) + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.add(:WATERVEIL, + proc { |ability,battler| + next if battler.status != :BURN + battler.battle.pbShowAbilitySplash(battler) + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battler.battle.pbDisplay(_INTL("{1}'s {2} healed its burn!",battler.pbThis,battler.abilityName)) + end + battler.battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::StatusCureAbility.copy(:WATERVEIL,:WATERBUBBLE) + +#=============================================================================== +# StatLossImmunityAbility handlers +#=============================================================================== + +BattleHandlers::StatLossImmunityAbility.add(:BIGPECKS, + proc { |ability,battler,stat,battle,showMessages| + next false if stat!=:DEFENSE + if showMessages + battle.pbShowAbilitySplash(battler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s {2} cannot be lowered!",battler.pbThis,GameData::Stat.get(stat).name)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents {3} loss!",battler.pbThis, + battler.abilityName,GameData::Stat.get(stat).name)) + end + battle.pbHideAbilitySplash(battler) + end + next true + } +) + +BattleHandlers::StatLossImmunityAbility.add(:CLEARBODY, + proc { |ability,battler,stat,battle,showMessages| + if showMessages + battle.pbShowAbilitySplash(battler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s stats cannot be lowered!",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents stat loss!",battler.pbThis,battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + end + next true + } +) + +BattleHandlers::StatLossImmunityAbility.copy(:CLEARBODY,:WHITESMOKE) + +BattleHandlers::StatLossImmunityAbility.add(:FLOWERVEIL, + proc { |ability,battler,stat,battle,showMessages| + next false if !battler.pbHasType?(:GRASS) + if showMessages + battle.pbShowAbilitySplash(battler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s stats cannot be lowered!",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents stat loss!",battler.pbThis,battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + end + next true + } +) + +BattleHandlers::StatLossImmunityAbility.add(:HYPERCUTTER, + proc { |ability,battler,stat,battle,showMessages| + next false if stat!=:ATTACK + if showMessages + battle.pbShowAbilitySplash(battler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s {2} cannot be lowered!",battler.pbThis,GameData::Stat.get(stat).name)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents {3} loss!",battler.pbThis, + battler.abilityName,GameData::Stat.get(stat).name)) + end + battle.pbHideAbilitySplash(battler) + end + next true + } +) + +BattleHandlers::StatLossImmunityAbility.add(:KEENEYE, + proc { |ability,battler,stat,battle,showMessages| + next false if stat!=:ACCURACY + if showMessages + battle.pbShowAbilitySplash(battler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s {2} cannot be lowered!",battler.pbThis,GameData::Stat.get(stat).name)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents {3} loss!",battler.pbThis, + battler.abilityName,GameData::Stat.get(stat).name)) + end + battle.pbHideAbilitySplash(battler) + end + next true + } +) + +#=============================================================================== +# StatLossImmunityAbilityNonIgnorable handlers +#=============================================================================== + +BattleHandlers::StatLossImmunityAbilityNonIgnorable.add(:FULLMETALBODY, + proc { |ability,battler,stat,battle,showMessages| + if showMessages + battle.pbShowAbilitySplash(battler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s stats cannot be lowered!",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents stat loss!",battler.pbThis,battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + end + next true + } +) + +#=============================================================================== +# StatLossImmunityAllyAbility handlers +#=============================================================================== + +BattleHandlers::StatLossImmunityAllyAbility.add(:FLOWERVEIL, + proc { |ability,bearer,battler,stat,battle,showMessages| + next false if !battler.pbHasType?(:GRASS) + if showMessages + battle.pbShowAbilitySplash(bearer) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s stats cannot be lowered!",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} prevents {3}'s stat loss!", + bearer.pbThis,bearer.abilityName,battler.pbThis(true))) + end + battle.pbHideAbilitySplash(bearer) + end + next true + } +) + +#=============================================================================== +# AbilityOnStatGain handlers +#=============================================================================== + +# There aren't any! + +#=============================================================================== +# AbilityOnStatLoss handlers +#=============================================================================== + +BattleHandlers::AbilityOnStatLoss.add(:COMPETITIVE, + proc { |ability,battler,stat,user| + next if user && !user.opposes?(battler) + battler.pbRaiseStatStageByAbility(:SPECIAL_ATTACK,2,battler,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::AbilityOnStatLoss.add(:DEFIANT, + proc { |ability,battler,stat,user| + next if user && !user.opposes?(battler) + battler.pbRaiseStatStageByAbility(:ATTACK,2,battler,GameData::Ability.get(ability).real_name) + } +) + +#=============================================================================== +# PriorityChangeAbility handlers +#=============================================================================== + +BattleHandlers::PriorityChangeAbility.add(:GALEWINGS, + proc { |ability,battler,move,pri| + next pri+1 if battler.hp==battler.totalhp && move.type == :FLYING + } +) + +BattleHandlers::PriorityChangeAbility.add(:PRANKSTER, + proc { |ability,battler,move,pri| + if move.statusMove? + battler.effects[PBEffects::Prankster] = true + next pri+1 + end + } +) + +BattleHandlers::PriorityChangeAbility.add(:TRIAGE, + proc { |ability,battler,move,pri| + next pri+3 if move.healingMove? + } +) + +#=============================================================================== +# PriorityBracketChangeAbility handlers +#=============================================================================== + +BattleHandlers::PriorityBracketChangeAbility.add(:STALL, + proc { |ability,battler,subPri,battle| + next -1 if subPri==0 + } +) + +#=============================================================================== +# PriorityBracketUseAbility handlers +#=============================================================================== + +# There aren't any! + +#=============================================================================== +# AbilityOnFlinch handlers +#=============================================================================== + +BattleHandlers::AbilityOnFlinch.add(:STEADFAST, + proc { |ability,battler,battle| + battler.pbRaiseStatStageByAbility(:SPEED,1,battler,GameData::Ability.get(ability).real_name) + } +) + +#=============================================================================== +# MoveBlockingAbility handlers +#=============================================================================== + +BattleHandlers::MoveBlockingAbility.add(:DAZZLING, + proc { |ability,bearer,user,targets,move,battle| + next false if battle.choices[user.index][4]<=0 + next false if !bearer.opposes?(user) + ret = false + targets.each do |b| + next if !b.opposes?(user) + ret = true + end + next ret + } +) + +BattleHandlers::MoveBlockingAbility.copy(:DAZZLING,:QUEENLYMAJESTY) + +#=============================================================================== +# MoveImmunityTargetAbility handlers +#=============================================================================== + +BattleHandlers::MoveImmunityTargetAbility.add(:BULLETPROOF, + proc { |ability,user,target,move,type,battle| + next false if !move.bombMove? + battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1}'s {2} made {3} ineffective!", + target.pbThis,target.abilityName,move.name)) + end + battle.pbHideAbilitySplash(target) + next true + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:FLASHFIRE, + proc { |ability,user,target,move,type,battle| + next false if user.index==target.index + next false if type != :FIRE + battle.pbShowAbilitySplash(target) + if !target.effects[PBEffects::FlashFire] + target.effects[PBEffects::FlashFire] = true + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("The power of {1}'s Fire-type moves rose!",target.pbThis(true))) + else + battle.pbDisplay(_INTL("The power of {1}'s Fire-type moves rose because of its {2}!", + target.pbThis(true),target.abilityName)) + end + else + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1}'s {2} made {3} ineffective!", + target.pbThis,target.abilityName,move.name)) + end + end + battle.pbHideAbilitySplash(target) + next true + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:LIGHTNINGROD, + proc { |ability,user,target,move,type,battle| + next pbBattleMoveImmunityStatAbility(user,target,move,type,:ELECTRIC,:SPECIAL_ATTACK,1,battle) + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:MOTORDRIVE, + proc { |ability,user,target,move,type,battle| + next pbBattleMoveImmunityStatAbility(user,target,move,type,:ELECTRIC,:SPEED,1,battle) + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:SAPSIPPER, + proc { |ability,user,target,move,type,battle| + next pbBattleMoveImmunityStatAbility(user,target,move,type,:GRASS,:ATTACK,1,battle) + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:SOUNDPROOF, + proc { |ability,user,target,move,type,battle| + next false if !move.soundMove? + battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1}'s {2} blocks {3}!",target.pbThis,target.abilityName,move.name)) + end + battle.pbHideAbilitySplash(target) + next true + + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:STORMDRAIN, + proc { |ability,user,target,move,type,battle| + next pbBattleMoveImmunityStatAbility(user,target,move,type,:WATER,:SPECIAL_ATTACK,1,battle) + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:TELEPATHY, + proc { |ability,user,target,move,type,battle| + next false if move.statusMove? + next false if user.index==target.index || target.opposes?(user) + battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} avoids attacks by its ally Pokémon!",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1} avoids attacks by its ally Pokémon with {2}!", + target.pbThis,target.abilityName)) + end + battle.pbHideAbilitySplash(target) + next true + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:VOLTABSORB, + proc { |ability,user,target,move,type,battle| + next pbBattleMoveImmunityHealAbility(user,target,move,type,:ELECTRIC,battle) + } +) + +BattleHandlers::MoveImmunityTargetAbility.add(:WATERABSORB, + proc { |ability,user,target,move,type,battle| + next pbBattleMoveImmunityHealAbility(user,target,move,type,:WATER,battle) + } +) + +BattleHandlers::MoveImmunityTargetAbility.copy(:WATERABSORB,:DRYSKIN) + +BattleHandlers::MoveImmunityTargetAbility.add(:WONDERGUARD, + proc { |ability,user,target,move,type,battle| + next false if move.statusMove? + next false if !type || Effectiveness.super_effective?(target.damageState.typeMod) + battle.pbShowAbilitySplash(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("It doesn't affect {1}...",target.pbThis(true))) + else + battle.pbDisplay(_INTL("{1} avoided damage with {2}!",target.pbThis,target.abilityName)) + end + battle.pbHideAbilitySplash(target) + next true + } +) + +#=============================================================================== +# MoveBaseTypeModifierAbility handlers +#=============================================================================== + +BattleHandlers::MoveBaseTypeModifierAbility.add(:AERILATE, + proc { |ability,user,move,type| + next if type != :NORMAL || !GameData::Type.exists?(:FLYING) + move.powerBoost = true + next :FLYING + } +) + +BattleHandlers::MoveBaseTypeModifierAbility.add(:GALVANIZE, + proc { |ability,user,move,type| + next if type != :NORMAL || !GameData::Type.exists?(:ELECTRIC) + move.powerBoost = true + next :ELECTRIC + } +) + +BattleHandlers::MoveBaseTypeModifierAbility.add(:LIQUIDVOICE, + proc { |ability,user,move,type| + next :WATER if GameData::Type.exists?(:WATER) && move.soundMove? + } +) + +BattleHandlers::MoveBaseTypeModifierAbility.add(:NORMALIZE, + proc { |ability,user,move,type| + next if !GameData::Type.exists?(:NORMAL) + move.powerBoost = true if Settings::MECHANICS_GENERATION >= 7 + next :NORMAL + } +) + +BattleHandlers::MoveBaseTypeModifierAbility.add(:PIXILATE, + proc { |ability,user,move,type| + next if type != :NORMAL || !GameData::Type.exists?(:FAIRY) + move.powerBoost = true + next :FAIRY + } +) + +BattleHandlers::MoveBaseTypeModifierAbility.add(:REFRIGERATE, + proc { |ability,user,move,type| + next if type != :NORMAL || !GameData::Type.exists?(:ICE) + move.powerBoost = true + next :ICE + } +) + +#=============================================================================== +# AccuracyCalcUserAbility handlers +#=============================================================================== + +BattleHandlers::AccuracyCalcUserAbility.add(:COMPOUNDEYES, + proc { |ability,mods,user,target,move,type| + mods[:accuracy_multiplier] *= 1.3 + } +) + +BattleHandlers::AccuracyCalcUserAbility.add(:HUSTLE, + proc { |ability,mods,user,target,move,type| + mods[:accuracy_multiplier] *= 0.8 if move.physicalMove? + } +) + +BattleHandlers::AccuracyCalcUserAbility.add(:KEENEYE, + proc { |ability,mods,user,target,move,type| + mods[:evasion_stage] = 0 if mods[:evasion_stage] > 0 && Settings::MECHANICS_GENERATION >= 6 + } +) + +BattleHandlers::AccuracyCalcUserAbility.add(:NOGUARD, + proc { |ability,mods,user,target,move,type| + mods[:base_accuracy] = 0 + } +) + +BattleHandlers::AccuracyCalcUserAbility.add(:UNAWARE, + proc { |ability,mods,user,target,move,type| + mods[:evasion_stage] = 0 if move.damagingMove? + } +) + +BattleHandlers::AccuracyCalcUserAbility.add(:VICTORYSTAR, + proc { |ability,mods,user,target,move,type| + mods[:accuracy_multiplier] *= 1.1 + } +) + +#=============================================================================== +# AccuracyCalcUserAllyAbility handlers +#=============================================================================== + +BattleHandlers::AccuracyCalcUserAllyAbility.add(:VICTORYSTAR, + proc { |ability,mods,user,target,move,type| + mods[:accuracy_multiplier] *= 1.1 + } +) + +#=============================================================================== +# AccuracyCalcTargetAbility handlers +#=============================================================================== + +BattleHandlers::AccuracyCalcTargetAbility.add(:LIGHTNINGROD, + proc { |ability,mods,user,target,move,type| + mods[:base_accuracy] = 0 if type == :ELECTRIC + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:NOGUARD, + proc { |ability,mods,user,target,move,type| + mods[:base_accuracy] = 0 + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:SANDVEIL, + proc { |ability,mods,user,target,move,type| + mods[:evasion_multiplier] *= 1.25 if target.battle.pbWeather == :Sandstorm + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:SNOWCLOAK, + proc { |ability,mods,user,target,move,type| + mods[:evasion_multiplier] *= 1.25 if target.battle.pbWeather == :Hail + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:STORMDRAIN, + proc { |ability,mods,user,target,move,type| + mods[:base_accuracy] = 0 if type == :WATER + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:TANGLEDFEET, + proc { |ability,mods,user,target,move,type| + mods[:accuracy_multiplier] /= 2 if target.effects[PBEffects::Confusion] > 0 + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:UNAWARE, + proc { |ability,mods,user,target,move,type| + mods[:accuracy_stage] = 0 if move.damagingMove? + } +) + +BattleHandlers::AccuracyCalcTargetAbility.add(:WONDERSKIN, + proc { |ability,mods,user,target,move,type| + if move.statusMove? && user.opposes?(target) + mods[:base_accuracy] = 50 if mods[:base_accuracy] > 50 + end + } +) + +#=============================================================================== +# DamageCalcUserAbility handlers +#=============================================================================== + +BattleHandlers::DamageCalcUserAbility.add(:AERILATE, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.2 if move.powerBoost + } +) + +BattleHandlers::DamageCalcUserAbility.copy(:AERILATE,:PIXILATE,:REFRIGERATE,:GALVANIZE) + +BattleHandlers::DamageCalcUserAbility.add(:ANALYTIC, + proc { |ability,user,target,move,mults,baseDmg,type| + if (target.battle.choices[target.index][0]!=:UseMove && + target.battle.choices[target.index][0]!=:Shift) || + target.movedThisRound? + mults[:base_damage_multiplier] *= 1.3 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:BLAZE, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.hp <= user.totalhp / 3 && type == :FIRE + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:DEFEATIST, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] /= 2 if user.hp <= user.totalhp / 2 + } +) + +BattleHandlers::DamageCalcUserAbility.add(:FLAREBOOST, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.burned? && move.specialMove? + mults[:base_damage_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:FLASHFIRE, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.effects[PBEffects::FlashFire] && type == :FIRE + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:FLOWERGIFT, + proc { |ability,user,target,move,mults,baseDmg,type| + if move.physicalMove? && [:Sun, :HarshSun].include?(user.battle.pbWeather) + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:GUTS, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.pbHasAnyStatus? && move.physicalMove? + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:HUGEPOWER, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] *= 2 if move.physicalMove? + } +) + +BattleHandlers::DamageCalcUserAbility.copy(:HUGEPOWER,:PUREPOWER) + +BattleHandlers::DamageCalcUserAbility.add(:HUSTLE, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] *= 1.5 if move.physicalMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:IRONFIST, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.2 if move.punchingMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:MEGALAUNCHER, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.5 if move.pulseMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:MINUS, + proc { |ability,user,target,move,mults,baseDmg,type| + next if !move.specialMove? + user.eachAlly do |b| + next if !b.hasActiveAbility?([:MINUS, :PLUS]) + mults[:attack_multiplier] *= 1.5 + break + end + } +) + +BattleHandlers::DamageCalcUserAbility.copy(:MINUS,:PLUS) + +BattleHandlers::DamageCalcUserAbility.add(:NEUROFORCE, + proc { |ability,user,target,move,mults,baseDmg,type| + if Effectiveness.super_effective?(target.damageState.typeMod) + mults[:final_damage_multiplier] *= 1.25 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:OVERGROW, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.hp <= user.totalhp / 3 && type == :GRASS + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:RECKLESS, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.2 if move.recoilMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:RIVALRY, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.gender!=2 && target.gender!=2 + if user.gender==target.gender + mults[:base_damage_multiplier] *= 1.25 + else + mults[:base_damage_multiplier] *= 0.75 + end + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:SANDFORCE, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.battle.pbWeather == :Sandstorm && + [:ROCK, :GROUND, :STEEL].include?(type) + mults[:base_damage_multiplier] *= 1.3 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:SHEERFORCE, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.3 if move.addlEffect > 0 + } +) + +BattleHandlers::DamageCalcUserAbility.add(:SLOWSTART, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] /= 2 if user.effects[PBEffects::SlowStart] > 0 && move.physicalMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:SOLARPOWER, + proc { |ability,user,target,move,mults,baseDmg,type| + if move.specialMove? && [:Sun, :HarshSun].include?(user.battle.pbWeather) + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:SNIPER, + proc { |ability,user,target,move,mults,baseDmg,type| + if target.damageState.critical + mults[:final_damage_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:STAKEOUT, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] *= 2 if target.battle.choices[target.index][0] == :SwitchOut + } +) + +BattleHandlers::DamageCalcUserAbility.add(:STEELWORKER, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] *= 1.5 if type == :STEEL + } +) + +BattleHandlers::DamageCalcUserAbility.add(:STRONGJAW, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.5 if move.bitingMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:SWARM, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.hp <= user.totalhp / 3 && type == :BUG + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:TECHNICIAN, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.index != target.index && move && move.id != :STRUGGLE && + baseDmg * mults[:base_damage_multiplier] <= 60 + mults[:base_damage_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:TINTEDLENS, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:final_damage_multiplier] *= 2 if Effectiveness.resistant?(target.damageState.typeMod) + } +) + +BattleHandlers::DamageCalcUserAbility.add(:TORRENT, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.hp <= user.totalhp / 3 && type == :WATER + mults[:attack_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:TOUGHCLAWS, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 4 / 3.0 if move.contactMove? + } +) + +BattleHandlers::DamageCalcUserAbility.add(:TOXICBOOST, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.poisoned? && move.physicalMove? + mults[:base_damage_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcUserAbility.add(:WATERBUBBLE, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:attack_multiplier] *= 2 if type == :WATER + } +) + +#=============================================================================== +# DamageCalcUserAllyAbility handlers +#=============================================================================== + +BattleHandlers::DamageCalcUserAllyAbility.add(:BATTERY, + proc { |ability,user,target,move,mults,baseDmg,type| + next if !move.specialMove? + mults[:final_damage_multiplier] *= 1.3 + } +) + +BattleHandlers::DamageCalcUserAllyAbility.add(:FLOWERGIFT, + proc { |ability,user,target,move,mults,baseDmg,type| + if move.physicalMove? && [:Sun, :HarshSun].include?(user.battle.pbWeather) + mults[:attack_multiplier] *= 1.5 + end + } +) + +#=============================================================================== +# DamageCalcTargetAbility handlers +#=============================================================================== + +BattleHandlers::DamageCalcTargetAbility.add(:DRYSKIN, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] *= 1.25 if type == :FIRE + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:FILTER, + proc { |ability,user,target,move,mults,baseDmg,type| + if Effectiveness.super_effective?(target.damageState.typeMod) + mults[:final_damage_multiplier] *= 0.75 + end + } +) + +BattleHandlers::DamageCalcTargetAbility.copy(:FILTER,:SOLIDROCK) + +BattleHandlers::DamageCalcTargetAbility.add(:FLOWERGIFT, + proc { |ability,user,target,move,mults,baseDmg,type| + if move.specialMove? && [:Sun, :HarshSun].include?(user.battle.pbWeather) + mults[:defense_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:FLUFFY, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:final_damage_multiplier] *= 2 if move.calcType == :FIRE + mults[:final_damage_multiplier] /= 2 if move.contactMove? + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:FURCOAT, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:defense_multiplier] *= 2 if move.physicalMove? || move.function == "122" # Psyshock + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:GRASSPELT, + proc { |ability,user,target,move,mults,baseDmg,type| + if user.battle.field.terrain == :Grassy + mults[:defense_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:HEATPROOF, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] /= 2 if type == :FIRE + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:MARVELSCALE, + proc { |ability,user,target,move,mults,baseDmg,type| + if target.pbHasAnyStatus? && move.physicalMove? + mults[:defense_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:MULTISCALE, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:final_damage_multiplier] /= 2 if target.hp == target.totalhp + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:THICKFAT, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:base_damage_multiplier] /= 2 if type == :FIRE || type == :ICE + } +) + +BattleHandlers::DamageCalcTargetAbility.add(:WATERBUBBLE, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:final_damage_multiplier] /= 2 if type == :FIRE + } +) + +#=============================================================================== +# DamageCalcTargetAbilityNonIgnorable handlers +#=============================================================================== + +BattleHandlers::DamageCalcTargetAbilityNonIgnorable.add(:PRISMARMOR, + proc { |ability,user,target,move,mults,baseDmg,type| + if Effectiveness.super_effective?(target.damageState.typeMod) + mults[:final_damage_multiplier] *= 0.75 + end + } +) + +BattleHandlers::DamageCalcTargetAbilityNonIgnorable.add(:SHADOWSHIELD, + proc { |ability,user,target,move,mults,baseDmg,type| + if target.hp==target.totalhp + mults[:final_damage_multiplier] /= 2 + end + } +) + +#=============================================================================== +# DamageCalcTargetAllyAbility handlers +#=============================================================================== + +BattleHandlers::DamageCalcTargetAllyAbility.add(:FLOWERGIFT, + proc { |ability,user,target,move,mults,baseDmg,type| + if move.specialMove? && [:Sun, :HarshSun].include?(user.battle.pbWeather) + mults[:defense_multiplier] *= 1.5 + end + } +) + +BattleHandlers::DamageCalcTargetAllyAbility.add(:FRIENDGUARD, + proc { |ability,user,target,move,mults,baseDmg,type| + mults[:final_damage_multiplier] *= 0.75 + } +) + +#=============================================================================== +# CriticalCalcUserAbility handlers +#=============================================================================== + +BattleHandlers::CriticalCalcUserAbility.add(:MERCILESS, + proc { |ability,user,target,c| + next 99 if target.poisoned? + } +) + +BattleHandlers::CriticalCalcUserAbility.add(:SUPERLUCK, + proc { |ability,user,target,c| + next c+1 + } +) + +#=============================================================================== +# CriticalCalcTargetAbility handlers +#=============================================================================== + +BattleHandlers::CriticalCalcTargetAbility.add(:BATTLEARMOR, + proc { |ability,user,target,c| + next -1 + } +) + +BattleHandlers::CriticalCalcTargetAbility.copy(:BATTLEARMOR,:SHELLARMOR) + +#=============================================================================== +# TargetAbilityOnHit handlers +#=============================================================================== + +BattleHandlers::TargetAbilityOnHit.add(:AFTERMATH, + proc { |ability,user,target,move,battle| + next if !target.fainted? + next if !move.pbContactMove?(user) + battle.pbShowAbilitySplash(target) + if !battle.moldBreaker + dampBattler = battle.pbCheckGlobalAbility(:DAMP) + if dampBattler + battle.pbShowAbilitySplash(dampBattler) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} cannot use {2}!",target.pbThis,target.abilityName)) + else + battle.pbDisplay(_INTL("{1} cannot use {2} because of {3}'s {4}!", + target.pbThis,target.abilityName,dampBattler.pbThis(true),dampBattler.abilityName)) + end + battle.pbHideAbilitySplash(dampBattler) + battle.pbHideAbilitySplash(target) + next + end + end + if user.takesIndirectDamage?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + battle.scene.pbDamageAnimation(user) + user.pbReduceHP(user.totalhp/4,false) + battle.pbDisplay(_INTL("{1} was caught in the aftermath!",user.pbThis)) + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:ANGERPOINT, + proc { |ability,user,target,move,battle| + next if !target.damageState.critical + next if !target.pbCanRaiseStatStage?(:ATTACK,target) + battle.pbShowAbilitySplash(target) + target.stages[:ATTACK] = 6 + battle.pbCommonAnimation("StatUp",target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} maxed its {2}!",target.pbThis,GameData::Stat.get(:ATTACK).name)) + else + battle.pbDisplay(_INTL("{1}'s {2} maxed its {3}!", + target.pbThis,target.abilityName,GameData::Stat.get(:ATTACK).name)) + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:CURSEDBODY, + proc { |ability,user,target,move,battle| + next if user.fainted? + next if user.effects[PBEffects::Disable]>0 + regularMove = nil + user.eachMove do |m| + next if m.id!=user.lastRegularMoveUsed + regularMove = m + break + end + next if !regularMove || (regularMove.pp==0 && regularMove.total_pp>0) + next if battle.pbRandom(100)>=30 + battle.pbShowAbilitySplash(target) + if !move.pbMoveFailedAromaVeil?(target,user,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + user.effects[PBEffects::Disable] = 3 + user.effects[PBEffects::DisableMove] = regularMove.id + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s {2} was disabled!",user.pbThis,regularMove.name)) + else + battle.pbDisplay(_INTL("{1}'s {2} was disabled by {3}'s {4}!", + user.pbThis,regularMove.name,target.pbThis(true),target.abilityName)) + end + battle.pbHideAbilitySplash(target) + user.pbItemStatusCureCheck + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:CUTECHARM, + proc { |ability,user,target,move,battle| + next if target.fainted? + next if !move.pbContactMove?(user) + next if battle.pbRandom(100)>=30 + battle.pbShowAbilitySplash(target) + if user.pbCanAttract?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} made {3} fall in love!",target.pbThis, + target.abilityName,user.pbThis(true)) + end + user.pbAttract(target,msg) + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:EFFECTSPORE, + proc { |ability,user,target,move,battle| + # NOTE: This ability has a 30% chance of triggering, not a 30% chance of + # inflicting a status condition. It can try (and fail) to inflict a + # status condition that the user is immune to. + next if !move.pbContactMove?(user) + next if battle.pbRandom(100)>=30 + r = battle.pbRandom(3) + next if r==0 && user.asleep? + next if r==1 && user.poisoned? + next if r==2 && user.paralyzed? + battle.pbShowAbilitySplash(target) + if user.affectedByPowder?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + case r + when 0 + if user.pbCanSleep?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} made {3} fall asleep!",target.pbThis, + target.abilityName,user.pbThis(true)) + end + user.pbSleep(msg) + end + when 1 + if user.pbCanPoison?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} poisoned {3}!",target.pbThis, + target.abilityName,user.pbThis(true)) + end + user.pbPoison(target,msg) + end + when 2 + if user.pbCanParalyze?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} paralyzed {3}! It may be unable to move!", + target.pbThis,target.abilityName,user.pbThis(true)) + end + user.pbParalyze(target,msg) + end + end + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:FLAMEBODY, + proc { |ability,user,target,move,battle| + next if !move.pbContactMove?(user) + next if user.burned? || battle.pbRandom(100)>=30 + battle.pbShowAbilitySplash(target) + if user.pbCanBurn?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} burned {3}!",target.pbThis,target.abilityName,user.pbThis(true)) + end + user.pbBurn(target,msg) + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:GOOEY, + proc { |ability,user,target,move,battle| + next if !move.pbContactMove?(user) + user.pbLowerStatStageByAbility(:SPEED,1,target,true,true,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::TargetAbilityOnHit.copy(:GOOEY,:TANGLINGHAIR) + +BattleHandlers::TargetAbilityOnHit.add(:ILLUSION, + proc { |ability,user,target,move,battle| + # NOTE: This intentionally doesn't show the ability splash. + next if !target.effects[PBEffects::Illusion] + target.effects[PBEffects::Illusion] = nil + battle.scene.pbChangePokemon(target,target.pokemon) + battle.pbDisplay(_INTL("{1}'s illusion wore off!",target.pbThis)) + battle.pbSetSeen(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:INNARDSOUT, + proc { |ability,user,target,move,battle| + next if !target.fainted? || user.dummy + battle.pbShowAbilitySplash(target) + if user.takesIndirectDamage?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + battle.scene.pbDamageAnimation(user) + user.pbReduceHP(target.damageState.hpLost,false) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} is hurt!",user.pbThis)) + else + battle.pbDisplay(_INTL("{1} is hurt by {2}'s {3}!",user.pbThis, + target.pbThis(true),target.abilityName)) + end + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:IRONBARBS, + proc { |ability,user,target,move,battle| + next if !move.pbContactMove?(user) + battle.pbShowAbilitySplash(target) + if user.takesIndirectDamage?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + battle.scene.pbDamageAnimation(user) + user.pbReduceHP(user.totalhp/8,false) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} is hurt!",user.pbThis)) + else + battle.pbDisplay(_INTL("{1} is hurt by {2}'s {3}!",user.pbThis, + target.pbThis(true),target.abilityName)) + end + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.copy(:IRONBARBS,:ROUGHSKIN) + +BattleHandlers::TargetAbilityOnHit.add(:JUSTIFIED, + proc { |ability,user,target,move,battle| + next if move.calcType != :DARK + target.pbRaiseStatStageByAbility(:ATTACK,1,target,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:MUMMY, + proc { |ability,user,target,move,battle| + next if !move.pbContactMove?(user) + next if user.fainted? + next if user.unstoppableAbility? || user.ability == ability + oldAbil = nil + battle.pbShowAbilitySplash(target) if user.opposes?(target) + if user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + oldAbil = user.ability + battle.pbShowAbilitySplash(user,true,false) if user.opposes?(target) + user.ability = ability + battle.pbReplaceAbilitySplash(user) if user.opposes?(target) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s Ability became {2}!",user.pbThis,user.abilityName)) + else + battle.pbDisplay(_INTL("{1}'s Ability became {2} because of {3}!", + user.pbThis,user.abilityName,target.pbThis(true))) + end + battle.pbHideAbilitySplash(user) if user.opposes?(target) + end + battle.pbHideAbilitySplash(target) if user.opposes?(target) + user.pbOnAbilityChanged(oldAbil) if oldAbil != nil + } +) + +BattleHandlers::TargetAbilityOnHit.add(:POISONPOINT, + proc { |ability,user,target,move,battle| + next if !move.pbContactMove?(user) + next if user.poisoned? || battle.pbRandom(100)>=30 + battle.pbShowAbilitySplash(target) + if user.pbCanPoison?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} poisoned {3}!",target.pbThis,target.abilityName,user.pbThis(true)) + end + user.pbPoison(target,msg) + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:RATTLED, + proc { |ability,user,target,move,battle| + next if ![:BUG, :DARK, :GHOST].include?(move.calcType) + target.pbRaiseStatStageByAbility(:SPEED,1,target,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:STAMINA, + proc { |ability,user,target,move,battle| + target.pbRaiseStatStageByAbility(:DEFENSE,1,target,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:STATIC, + proc { |ability,user,target,move,battle| + next if !move.pbContactMove?(user) + next if user.paralyzed? || battle.pbRandom(100)>=30 + battle.pbShowAbilitySplash(target) + if user.pbCanParalyze?(target,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) && + user.affectedByContactEffect?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} paralyzed {3}! It may be unable to move!", + target.pbThis,target.abilityName,user.pbThis(true)) + end + user.pbParalyze(target,msg) + end + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:WATERCOMPACTION, + proc { |ability,user,target,move,battle| + next if move.calcType != :WATER + target.pbRaiseStatStageByAbility(:DEFENSE,2,target,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::TargetAbilityOnHit.add(:WEAKARMOR, + proc { |ability,user,target,move,battle| + next if !move.physicalMove? + next if !target.pbCanLowerStatStage?(:DEFENSE, target) && + !target.pbCanRaiseStatStage?(:SPEED, target) + battle.pbShowAbilitySplash(target) + target.pbLowerStatStageByAbility(:DEFENSE, 1, target, false,GameData::Ability.get(ability).real_name) + target.pbRaiseStatStageByAbility(:SPEED, + (Settings::MECHANICS_GENERATION >= 7) ? 2 : 1, target, false,GameData::Ability.get(ability).real_name) + battle.pbHideAbilitySplash(target) + } +) + +#=============================================================================== +# UserAbilityOnHit handlers +#=============================================================================== + +BattleHandlers::UserAbilityOnHit.add(:POISONTOUCH, + proc { |ability,user,target,move,battle| + next if !move.contactMove? + next if battle.pbRandom(100)>=30 + battle.pbShowAbilitySplash(user) + if target.hasActiveAbility?(:SHIELDDUST) && !battle.moldBreaker + battle.pbShowAbilitySplash(target) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} is unaffected!",target.pbThis)) + end + battle.pbHideAbilitySplash(target) + elsif target.pbCanPoison?(user,PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + msg = nil + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + msg = _INTL("{1}'s {2} poisoned {3}!",user.pbThis,user.abilityName,target.pbThis(true)) + end + target.pbPoison(user,msg) + end + battle.pbHideAbilitySplash(user) + } +) + +#=============================================================================== +# UserAbilityEndOfMove handlers +#=============================================================================== + +BattleHandlers::UserAbilityEndOfMove.add(:BEASTBOOST, + proc { |ability,user,targets,move,battle| + next if battle.pbAllFainted?(user.idxOpposingSide) + numFainted = 0 + targets.each { |b| numFainted += 1 if b.damageState.fainted } + next if numFainted == 0 + userStats = user.plainStats + highestStatValue = 0 + userStats.each_value { |value| highestStatValue = value if highestStatValue < value } + GameData::Stat.each_main_battle do |s| + next if userStats[s.id] < highestStatValue + if user.pbCanRaiseStatStage?(s.id, user) + user.pbRaiseStatStageByAbility(s.id, numFainted, user,GameData::Ability.get(ability).real_name) + end + break + end + } +) + +BattleHandlers::UserAbilityEndOfMove.add(:MAGICIAN, + proc { |ability,user,targets,move,battle| + next if battle.futureSight + next if !move.pbDamagingMove? + next if user.item + next if battle.wildBattle? && user.opposes? + targets.each do |b| + next if b.damageState.unaffected || b.damageState.substitute + next if !b.item + next if b.unlosableItem?(b.item) || user.unlosableItem?(b.item) + battle.pbShowAbilitySplash(user) + if b.hasActiveAbility?(:STICKYHOLD) + battle.pbShowAbilitySplash(b) if user.opposes?(b) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s item cannot be stolen!",b.pbThis)) + end + battle.pbHideAbilitySplash(b) if user.opposes?(b) + next + end + user.item = b.item + b.item = nil + b.effects[PBEffects::Unburden] = true + if battle.wildBattle? && !user.initialItem && b.initialItem==user.item + user.setInitialItem(user.item) + b.setInitialItem(nil) + end + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} stole {2}'s {3}!",user.pbThis, + b.pbThis(true),user.itemName)) + else + battle.pbDisplay(_INTL("{1} stole {2}'s {3} with {4}!",user.pbThis, + b.pbThis(true),user.itemName,user.abilityName)) + end + battle.pbHideAbilitySplash(user) + user.pbHeldItemTriggerCheck + break + end + } +) + +BattleHandlers::UserAbilityEndOfMove.add(:MOXIE, + proc { |ability,user,targets,move,battle| + next if battle.pbAllFainted?(user.idxOpposingSide) + numFainted = 0 + targets.each { |b| numFainted += 1 if b.damageState.fainted } + next if numFainted==0 || !user.pbCanRaiseStatStage?(:ATTACK,user) + user.pbRaiseStatStageByAbility(:ATTACK,numFainted,user,GameData::Ability.get(ability).real_name) + } +) + +#=============================================================================== +# TargetAbilityAfterMoveUse handlers +#=============================================================================== + +BattleHandlers::TargetAbilityAfterMoveUse.add(:BERSERK, + proc { |ability,target,user,move,switched,battle| + next if !move.damagingMove? + next if target.damageState.initialHP=target.totalhp/2 + next if !target.pbCanRaiseStatStage?(:SPECIAL_ATTACK,target) + target.pbRaiseStatStageByAbility(:SPECIAL_ATTACK,1,target,GameData::Ability.get(ability).real_name) + } +) + +BattleHandlers::TargetAbilityAfterMoveUse.add(:COLORCHANGE, + proc { |ability,target,user,move,switched,battle| + next if target.damageState.calcDamage==0 || target.damageState.substitute + next if !move.calcType || GameData::Type.get(move.calcType).pseudo_type + next if target.pbHasType?(move.calcType) && !target.pbHasOtherType?(move.calcType) + typeName = GameData::Type.get(move.calcType).name + battle.pbShowAbilitySplash(target) + target.pbChangeTypes(move.calcType) + battle.pbDisplay(_INTL("{1}'s {2} made it the {3} type!",target.pbThis, + target.abilityName,typeName)) + battle.pbHideAbilitySplash(target) + } +) + +BattleHandlers::TargetAbilityAfterMoveUse.add(:PICKPOCKET, + proc { |ability,target,user,move,switched,battle| + # NOTE: According to Bulbapedia, this can still trigger to steal the user's + # item even if it was switched out by a Red Card. This doesn't make + # sense, so this code doesn't do it. + next if battle.wildBattle? && target.opposes? + next if !move.contactMove? + next if switched.include?(user.index) + next if user.effects[PBEffects::Substitute]>0 || target.damageState.substitute + next if target.item || !user.item + next if user.unlosableItem?(user.item) || target.unlosableItem?(user.item) + battle.pbShowAbilitySplash(target) + if user.hasActiveAbility?(:STICKYHOLD) + battle.pbShowAbilitySplash(user) if target.opposes?(user) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s item cannot be stolen!",user.pbThis)) + end + battle.pbHideAbilitySplash(user) if target.opposes?(user) + battle.pbHideAbilitySplash(target) + next + end + target.item = user.item + user.item = nil + user.effects[PBEffects::Unburden] = true + if battle.wildBattle? && !target.initialItem && user.initialItem==target.item + target.setInitialItem(target.item) + user.setInitialItem(nil) + end + battle.pbDisplay(_INTL("{1} pickpocketed {2}'s {3}!",target.pbThis, + user.pbThis(true),target.itemName)) + battle.pbHideAbilitySplash(target) + target.pbHeldItemTriggerCheck + } +) + +#=============================================================================== +# EORWeatherAbility handlers +#=============================================================================== + +BattleHandlers::EORWeatherAbility.add(:DRYSKIN, + proc { |ability,weather,battler,battle| + case weather + when :Sun, :HarshSun + battle.pbShowAbilitySplash(battler) + battle.scene.pbDamageAnimation(battler) + battler.pbReduceHP(battler.totalhp/8,false) + battle.pbDisplay(_INTL("{1} was hurt by the sunlight!",battler.pbThis)) + battle.pbHideAbilitySplash(battler) + battler.pbItemHPHealCheck + when :Rain, :HeavyRain + next if !battler.canHeal? + battle.pbShowAbilitySplash(battler) + battler.pbRecoverHP(battler.totalhp/8) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s HP was restored.",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} restored its HP.",battler.pbThis,battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + end + } +) + +BattleHandlers::EORWeatherAbility.add(:ICEBODY, + proc { |ability,weather,battler,battle| + next unless weather == :Hail + next if !battler.canHeal? + battle.pbShowAbilitySplash(battler) + battler.pbRecoverHP(battler.totalhp/16) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s HP was restored.",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} restored its HP.",battler.pbThis,battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::EORWeatherAbility.add(:RAINDISH, + proc { |ability,weather,battler,battle| + next unless [:Rain, :HeavyRain].include?(weather) + next if !battler.canHeal? + battle.pbShowAbilitySplash(battler) + battler.pbRecoverHP(battler.totalhp/16) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1}'s HP was restored.",battler.pbThis)) + else + battle.pbDisplay(_INTL("{1}'s {2} restored its HP.",battler.pbThis,battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::EORWeatherAbility.add(:SOLARPOWER, + proc { |ability,weather,battler,battle| + next unless [:Sun, :HarshSun].include?(weather) + battle.pbShowAbilitySplash(battler) + battle.scene.pbDamageAnimation(battler) + battler.pbReduceHP(battler.totalhp/8,false) + battle.pbDisplay(_INTL("{1} was hurt by the sunlight!",battler.pbThis)) + battle.pbHideAbilitySplash(battler) + battler.pbItemHPHealCheck + } +) + +#=============================================================================== +# EORHealingAbility handlers +#=============================================================================== + +BattleHandlers::EORHealingAbility.add(:HEALER, + proc { |ability,battler,battle| + next unless battle.pbRandom(100)<30 + battler.eachAlly do |b| + next if b.status == :NONE + battle.pbShowAbilitySplash(battler) + oldStatus = b.status + b.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + case oldStatus + when :SLEEP + battle.pbDisplay(_INTL("{1}'s {2} woke its partner up!",battler.pbThis,battler.abilityName)) + when :POISON + battle.pbDisplay(_INTL("{1}'s {2} cured its partner's poison!",battler.pbThis,battler.abilityName)) + when :BURN + battle.pbDisplay(_INTL("{1}'s {2} healed its partner's burn!",battler.pbThis,battler.abilityName)) + when :PARALYSIS + battle.pbDisplay(_INTL("{1}'s {2} cured its partner's paralysis!",battler.pbThis,battler.abilityName)) + when :FROZEN + battle.pbDisplay(_INTL("{1}'s {2} defrosted its partner!",battler.pbThis,battler.abilityName)) + end + end + battle.pbHideAbilitySplash(battler) + end + } +) + +BattleHandlers::EORHealingAbility.add(:HYDRATION, + proc { |ability,battler,battle| + next if battler.status == :NONE + next if ![:Rain, :HeavyRain].include?(battle.pbWeather) + battle.pbShowAbilitySplash(battler) + oldStatus = battler.status + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + case oldStatus + when :SLEEP + battle.pbDisplay(_INTL("{1}'s {2} woke it up!",battler.pbThis,battler.abilityName)) + when :POISON + battle.pbDisplay(_INTL("{1}'s {2} cured its poison!",battler.pbThis,battler.abilityName)) + when :BURN + battle.pbDisplay(_INTL("{1}'s {2} healed its burn!",battler.pbThis,battler.abilityName)) + when :PARALYSIS + battle.pbDisplay(_INTL("{1}'s {2} cured its paralysis!",battler.pbThis,battler.abilityName)) + when :FROZEN + battle.pbDisplay(_INTL("{1}'s {2} defrosted it!",battler.pbThis,battler.abilityName)) + end + end + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::EORHealingAbility.add(:SHEDSKIN, + proc { |ability,battler,battle| + next if battler.status == :NONE + next unless battle.pbRandom(100)<30 + battle.pbShowAbilitySplash(battler) + oldStatus = battler.status + battler.pbCureStatus(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + case oldStatus + when :SLEEP + battle.pbDisplay(_INTL("{1}'s {2} woke it up!",battler.pbThis,battler.abilityName)) + when :POISON + battle.pbDisplay(_INTL("{1}'s {2} cured its poison!",battler.pbThis,battler.abilityName)) + when :BURN + battle.pbDisplay(_INTL("{1}'s {2} healed its burn!",battler.pbThis,battler.abilityName)) + when :PARALYSIS + battle.pbDisplay(_INTL("{1}'s {2} cured its paralysis!",battler.pbThis,battler.abilityName)) + when :FROZEN + battle.pbDisplay(_INTL("{1}'s {2} defrosted it!",battler.pbThis,battler.abilityName)) + end + end + battle.pbHideAbilitySplash(battler) + } +) + +#=============================================================================== +# EOREffectAbility handlers +#=============================================================================== + +BattleHandlers::EOREffectAbility.add(:BADDREAMS, + proc { |ability,battler,battle| + battle.eachOtherSideBattler(battler.index) do |b| + next if !b.near?(battler) || !b.asleep? + battle.pbShowAbilitySplash(battler) + next if !b.takesIndirectDamage?(PokeBattle_SceneConstants::USE_ABILITY_SPLASH) + oldHP = b.hp + b.pbReduceHP(b.totalhp/8) + if PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} is tormented!",b.pbThis)) + else + battle.pbDisplay(_INTL("{1} is tormented by {2}'s {3}!",b.pbThis, + battler.pbThis(true),battler.abilityName)) + end + battle.pbHideAbilitySplash(battler) + b.pbItemHPHealCheck + b.pbAbilitiesOnDamageTaken(oldHP) + b.pbFaint if b.fainted? + end + } +) + +BattleHandlers::EOREffectAbility.add(:MOODY, + proc { |ability,battler,battle| + randomUp = [] + randomDown = [] + GameData::Stat.each_battle do |s| + randomUp.push(s.id) if battler.pbCanRaiseStatStage?(s.id, battler) + randomDown.push(s.id) if battler.pbCanLowerStatStage?(s.id, battler) + end + next if randomUp.length==0 && randomDown.length==0 + battle.pbShowAbilitySplash(battler) + if randomUp.length>0 + r = battle.pbRandom(randomUp.length) + battler.pbRaiseStatStageByAbility(randomUp[r],2,battler,false,GameData::Ability.get(ability).real_name) + randomDown.delete(randomUp[r]) + end + if randomDown.length>0 + r = battle.pbRandom(randomDown.length) + battler.pbLowerStatStageByAbility(randomDown[r],1,battler,false,GameData::Ability.get(ability).real_name) + end + battle.pbHideAbilitySplash(battler) + battler.pbItemStatRestoreCheck if randomDown.length>0 + } +) + +BattleHandlers::EOREffectAbility.add(:SPEEDBOOST, + proc { |ability,battler,battle| + # A Pokémon's turnCount is 0 if it became active after the beginning of a + # round + if battler.turnCount>0 && battler.pbCanRaiseStatStage?(:SPEED,battler) + ability_name = GameData::Ability.get(ability).real_name + battler.pbRaiseStatStageByAbility(:SPEED,1,battler,true,ability_name) + end + } +) + +#=============================================================================== +# EORGainItemAbility handlers +#=============================================================================== + +BattleHandlers::EORGainItemAbility.add(:HARVEST, + proc { |ability,battler,battle| + next if battler.item + next if !battler.recycleItem || !GameData::Item.get(battler.recycleItem).is_berry? + if ![:Sun, :HarshSun].include?(battle.pbWeather) + next unless battle.pbRandom(100)<50 + end + battle.pbShowAbilitySplash(battler) + battler.item = battler.recycleItem + battler.setRecycleItem(nil) + battler.setInitialItem(battler.item) if !battler.initialItem + battle.pbDisplay(_INTL("{1} harvested one {2}!",battler.pbThis,battler.itemName)) + battle.pbHideAbilitySplash(battler) + battler.pbHeldItemTriggerCheck + } +) + +BattleHandlers::EORGainItemAbility.add(:PICKUP, + proc { |ability,battler,battle| + next if battler.item + foundItem = nil; fromBattler = nil; use = 0 + battle.eachBattler do |b| + next if b.index==battler.index + next if b.effects[PBEffects::PickupUse]<=use + foundItem = b.effects[PBEffects::PickupItem] + fromBattler = b + use = b.effects[PBEffects::PickupUse] + end + next if !foundItem + battle.pbShowAbilitySplash(battler) + battler.item = foundItem + fromBattler.effects[PBEffects::PickupItem] = nil + fromBattler.effects[PBEffects::PickupUse] = 0 + fromBattler.setRecycleItem(nil) if fromBattler.recycleItem==foundItem + if battle.wildBattle? && !battler.initialItem && fromBattler.initialItem==foundItem + battler.setInitialItem(foundItem) + fromBattler.setInitialItem(nil) + end + battle.pbDisplay(_INTL("{1} found one {2}!",battler.pbThis,battler.itemName)) + battle.pbHideAbilitySplash(battler) + battler.pbHeldItemTriggerCheck + } +) + +#=============================================================================== +# CertainSwitchingUserAbility handlers +#=============================================================================== + +# There aren't any! + +#=============================================================================== +# TrappingTargetAbility handlers +#=============================================================================== + +BattleHandlers::TrappingTargetAbility.add(:ARENATRAP, + proc { |ability,switcher,bearer,battle| + next true if !switcher.airborne? + } +) + +BattleHandlers::TrappingTargetAbility.add(:MAGNETPULL, + proc { |ability,switcher,bearer,battle| + next true if switcher.pbHasType?(:STEEL) + } +) + +BattleHandlers::TrappingTargetAbility.add(:SHADOWTAG, + proc { |ability,switcher,bearer,battle| + next true if !switcher.hasActiveAbility?(:SHADOWTAG) + } +) + +#=============================================================================== +# AbilityOnSwitchIn handlers +#=============================================================================== + +BattleHandlers::AbilityOnSwitchIn.add(:AIRLOCK, + proc { |ability,battler,battle| + battle.pbShowAbilitySplash(battler) + if !PokeBattle_SceneConstants::USE_ABILITY_SPLASH + battle.pbDisplay(_INTL("{1} has {2}!",battler.pbThis,battler.abilityName)) + end + battle.pbDisplay(_INTL("The effects of the weather disappeared.")) + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::AbilityOnSwitchIn.copy(:AIRLOCK,:CLOUDNINE) + +BattleHandlers::AbilityOnSwitchIn.add(:ANTICIPATION, + proc { |ability,battler,battle| + next if !battler.pbOwnedByPlayer? + battlerTypes = battler.pbTypes(true) + type1 = battlerTypes[0] + type2 = battlerTypes[1] || type1 + type3 = battlerTypes[2] || type2 + found = false + battle.eachOtherSideBattler(battler.index) do |b| + b.eachMove do |m| + next if m.statusMove? + if type1 + moveType = m.type + if Settings::MECHANICS_GENERATION >= 6 && m.function == "090" # Hidden Power + moveType = pbHiddenPower(b.pokemon)[0] + end + eff = Effectiveness.calculate(moveType,type1,type2,type3) + next if Effectiveness.ineffective?(eff) + next if !Effectiveness.super_effective?(eff) && m.function != "070" # OHKO + else + next if m.function != "070" # OHKO + end + found = true + break + end + break if found + end + if found + battle.pbShowAbilitySplash(battler) + battle.pbDisplay(_INTL("{1} shuddered with anticipation!",battler.pbThis)) + battle.pbHideAbilitySplash(battler) + end + } +) + +BattleHandlers::AbilityOnSwitchIn.add(:AURABREAK, + proc { |ability,battler,battle| + battle.pbShowAbilitySplash(battler) + battle.pbDisplay(_INTL("{1} reversed all other Pokémon's auras!",battler.pbThis)) + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::AbilityOnSwitchIn.add(:COMATOSE, + proc { |ability,battler,battle| + battle.pbShowAbilitySplash(battler) + battle.pbDisplay(_INTL("{1} is drowsing!",battler.pbThis)) + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::AbilityOnSwitchIn.add(:DARKAURA, + proc { |ability,battler,battle| + battle.pbShowAbilitySplash(battler) + battle.pbDisplay(_INTL("{1} is radiating a dark aura!",battler.pbThis)) + battle.pbHideAbilitySplash(battler) + } +) + +BattleHandlers::AbilityOnSwitchIn.add(:DELTASTREAM, + proc { |ability,battler,battle| + pbBattleWeatherAbility(:StrongWinds, battler, battle, true) + } +) + +BattleHandlers::AbilityOnSwitchIn.add(:DESOLATELAND, + proc { |ability,battler,battle| + pbBattleWeatherAbility(:HarshSun, battler, battle, true) + } +) + +BattleHandlers::AbilityOnSwitchIn.add(:DOWNLOAD, + proc { |ability,battler,battle| + oDef = oSpDef = 0 + battle.eachOtherSideBattler(battler.index) do |b| + oDef += b.defense + oSpDef += b.spdef + end + stat = (oDef