• Do not use Discord to host any images you post, these links expire quickly! You can learn how to add images to your posts here.
Thinking through a script

any version Thinking through a script 2022-06-05

This resource does not pertain to any specific version of Pokémon Essentials.
Pokémon Essentials Version
Non-applicable
This tutorial isn't about working with Ruby or good coding practices or anything, but just a basic idea of planning out a script, figuring out how to write it, and making sure it works.

1654425146079.png
1 - Outline
1654425149659.png
Write out a step-by-step summary of how your script should work. Try to get specific with every detail and scenario. What conditions should be met for things to happen? Are there any branching paths in your script? (This can be especially important for scripts that involve player input) What's something that could go wrong?

1654425188329.png
2 - Research
1654425192294.png
Figure out how to write the individual steps in Ruby. Some of this will involve just a basic understanding of the language (Marin has a nice guide here), and other parts (usually the Pokémon-specific ones) will involve looking around to figure out how things are done in Essentials. You have lots of options here - the default maps, the wiki, and the script sections themselves. (They're named and grouped in a pretty easy-to-navigate way, but you can also search with Ctrl+Shift+F to search all sections at once) You can also try searching any Essentials-based forum/discord/etc. to see if anyone's asked for similar functions.

1654425260424.png
3 - Convert
1654425255678.png
Go through your summary, and translate every action into the code you found. Remember to close all your conditional branches - I personally find it helpful to include the end as soon as I add an if, so I don't have to worry about keeping track again later.

1654425334918.png
4 - Playtest
1654425323368.png
Set up your game to trigger what you want your script to do. Don't worry about making it exactly like it'll be in-game, set it up for your convenience. Put things close to where you're currently saved, use Ctrl to avoid events, give yourself Master Balls, use the debug menu to set switches and the like. (Or, heck, just make a quick event to set it up how you want)

When playtesting, try doing the wrong thing, and make sure things still get on the right track. This way, you can make sure to catch any errors you might not have considered when planning out how things should be.





As an example, I'm going to go step-by-step through my process and create a script. This is based on a concept by silentgamer64 way back in 2017 - a script that changes the weather when a roaming Pokémon is on a map, like with the Forces of Nature in Gen 5.

1654425146079.png
1 - Outline
1654425149659.png

When the player moves to a new map, the game checks if there's a roaming Pokémon on the map. If there is, then I should check if the roaming Pokémon is one of the species that should change the weather, and change the map's weather to that Pokémon's weather. If the roaming Pokémon moves to a new map, the weather should change back to normal.

Conditions that I'll need to check -
  • Is there a roaming Pokémon on the given map?
  • Is the current map outdoors?
  • Is it currently night?
  • Did I set a weather for this roaming Pokémon?
Some of these have specific calls, and others will involve me setting a variable and checking it later.

Branching paths
  • Depending on the Pokémon, I want different kinds of weather.
Other than that, I shouldn't need to worry about any more alternatives - this is something that shouldn't have much input on the player's end.

Ways this can go wrong
  • I only want weather in outdoor maps- I don't want weather in buildings, inside caves, or underwater.
  • I don't want to make it sunny at night.
  • If two roaming Pokémon are on the same map, and they both have their own unique weather, what weather should be used?
  • If I have a map with its own weather from metadata, I should make sure that it doesn't override the Pokémon's weather. (For example, if I had a winter map where it was snowing, I would want it to still rain if Tornadus appeared)
All of these are things that I could easily get around just by setting up the roaming Pokémon a certain way- make sure it can't spawn on indoor/cave/underwater maps, don't make it sunny, don't make the Pokémon spawn on maps with their own weather. But for the sake of this tutorial, I'll go ahead and address some of these issues in the code itself. I will, however, skip over the "maps with their own weather" issue for now, because I'm not sure it's going to be an issue. I'll need to playtest to be sure. I'll also skip over "two roaming Pokémon on the same map", because there's multiple possible solutions here. (You could just keep them to separate paths, you could make unique weathers for if two Pokémon are on the same map, you could have an order of priority, you could randomize it, you could make sure roaming Pokémon never land on the same map, etc.) For the purposes of this example, I'm going to treat this as if I've made it where a Pokémon will roam again if it's on the same map as another roamer.

So, here's how I'm writing out my summary now:

When the player enters a new map, the game should check if there's a roaming Pokémon. If there is, the game should check what species it is, and get its respective weather - storm for Thunderus, rain for Tornadus, etc. Then, the game should make sure that the current map is outdoors, and that it's not about to make it sunny at night. If these conditions are met, then the map's weather should change to the new weather, if there is a new weather. If the roaming Pokémon leaves, the map's weather should return to what it usually is based on the metadata.

There's one more possible problem. If I put in a weather ID that doesn't exist, the game will crash when it tries to change the weather. But I'm actually going to choose to leave this in - that way, if I put in the wrong ID, I'll get an error message about it when playtesting. If I just made it skip changing the weather if the ID doesn't exist, I could keep playing without any errors, but I might not realize something was wrong.

1654425188329.png
2 - Research
1654425192294.png

- I want to run this check every time the player changes maps.

I could look at the code for roaming Pokémon moving when the player changes maps, or the code for location signposts appearing. I'm going to look at the roaming one, just to keep to the same collection of scripts and make it easier on me. Luckily for me, roaming Pokémon have their own script section - Overworld_RoamingPokemon.

Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :move_roaming_pokemon,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    # Make roaming Pokémon roam
    pbRoamPokemon
    $PokemonGlobal.roamedAlready = false
  }
)
This is an event handler! I won't get into much detail here, but in v20, event handlers are structures like this -

EventHandlers.add(:trigger, :unique name,

So I'll be making my handler EventHandlers.add(:on_enter_map, :roaming_weather,

Now, this code is providing me with a new question I didn't think about before. According to this, Pokémon won't roam if the player is moving to another map with the same name as the one they were on. I should probably include that same check in this code, to keep the roaming mechanics consistent, and update my summary accordingly.

- I want to check if a roaming Pokémon is on the current map

Back to Overworld_RoamingPokemon!

Ruby:
Expand Collapse Copy
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Check whether the roamer's roamer method is currently possible
      next if !pbRoamingMethodAllowed(data[3])
      # Add this roaming Pokémon to the list of possible roaming Pokémon to encounter
      possible_roamers.push([i, data[0], data[1], data[4]])   # [i, species, level, BGM]
    end
There's a lot going on here! Luckily, there's plenty of comments for me to understand what's going on. What this code is doing is:
  • Going through each of the defined roamers. It skips if the species doesn't exist, if it's not roaming, or if it's already been caught.
  • Getting the roamer's current map. It skips if there aren't any maps for the Pokémon to roam on. If there are maps, but the Pokémon just isn't on one, it picks a random map for the Pokémon.
  • Check if the roamer is on the current map, or on a map with the same name in the same region.
  • Check if the encounter method is currently possible.
  • Adds the various data to the array.
I'll need most of this, but there's two things I think I can remove -
  • I don't need to check if the encounter method is currently possible - I'm checking if the Pokemon is on the map, not if the player is currently in grass/surfing/fishing.
  • I don't need to include most of the roaming data - I'm only checking the species. I also said earlier that I'm going to treat this as if only one roaming Pokémon can be on a map at a time, so I'm not going to bother with creating an array to sample it - I'll just get the species.
Here's what that looks like with my changes -
Ruby:
Expand Collapse Copy
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end


- I want to check the roaming Pokémon's species, and set another value depending on that.

Ordinarily, if I said "I want to check a Pokémon's species", I might take a look at an article like Manipulating Pokémon, which has checks to apply to an individual Pokémon. But my code to check for the species earlier just got the species directly from the roaming Pokémon data, and I saved it as the variable species. So I can just make a case statement to set a variable weather depending on what species is.

Ruby:
Expand Collapse Copy
case species
  when :GROUDON
    weather = :Sun
  when :KYOGRE
    weather = :HeavyRain
  when :TORNADUS
    weather = :Rain
  when :THUNDURUS
    weather = :Storm
end

- I want to check if a map is outdoors or not.

I know that you can't fly from indoor maps, so I'll check the handler for Fly in Overworld_FieldMoves and see if I can find that check there.
Ruby:
Expand Collapse Copy
def pbCanFly?(pkmn = nil, show_messages = false)
  return false if !pbCheckHiddenMoveBadge(Settings::BADGE_FOR_FLY, show_messages)
  return false if !$DEBUG && !pkmn && !$player.get_pokemon_with_move(:FLY)
  if !$game_player.can_map_transfer_with_follower?
    pbMessage(_INTL("It can't be used when you have someone with you.")) if show_messages
    return false
  end
  if !$game_map.metadata&.outdoor_map
    pbMessage(_INTL("You can't use that here.")) if show_messages
    return false
  end
  return true
end
There it is! $game_map.metadata&.outdoor_map returns true if the current map is outdoors, false if it isn't.

There is, of course, all kinds of other ways I could have found this - I could have also checked in the Time section, for the day/night tint, in the item handlers, for whether the player can use their bike, I could have looked at how a map's metadata is defined and figured out the method from there... This is just a simple example for if you're not familiar with coding from scratch, but you're familiar with base Pokémon mechanics.

- I want to check if it's currently nighttime

The wiki actually has an article about time, and it tells you to check out Overworld_Time to see all the methods defined there. You could also look in the default maps - on Route 7, a police offer event demonstrates checking if it's night via script switch.

PBDayNight.isNight?

- I want to change the map's weather

Also on Route 7, there's an event where an NPC can change the weather. Some of this is done via an event command, but the custom weather is done via a script command.
Ruby:
Expand Collapse Copy
$game_screen.weather(
  :HeavyRain, 9, 20
)
(The 9,20 are parameters, for the intensity of the weather and the number of frames it takes for the weather to pick up. I'm going to just leave them as-is for this script)

- I want to check if a roaming Pokémon has left a map

...Or, well, that's what I said. But, according to the wiki article on Roaming Pokémon, roaming Pokémon don't change maps until the player changes maps! I could change that if I wanted, but I'm going to leave it as-is for this tutorial. So my initial check will work just fine!

But just because the Pokémon won't go to a new map doesn't mean they'll still be on this one - what if the player defeats or catches them? I know that this is information stored as part of the roaming Pokémon data, so I'll look in that script section to see where it records that. It looks like it's done in def pbRoamingPokemonBattle.

Ruby:
Expand Collapse Copy
def pbRoamingPokemonBattle(species, level)
  # Get the roaming Pokémon to encounter; generate it based on the species and
  # level if it doesn't already exist
  idxRoamer = $game_temp.roamer_index_for_encounter
  if !$PokemonGlobal.roamPokemon[idxRoamer] ||
     !$PokemonGlobal.roamPokemon[idxRoamer].is_a?(Pokemon)
    $PokemonGlobal.roamPokemon[idxRoamer] = pbGenerateWildPokemon(species, level, true)
  end
  # Set some battle rules
  setBattleRule("single")
  setBattleRule("roamerFlees")
  # Perform the battle
  decision = WildBattle.start_core($PokemonGlobal.roamPokemon[idxRoamer])
  # Update Roaming Pokémon data based on result of battle
  if [1, 4].include?(decision)   # Defeated or caught
    $PokemonGlobal.roamPokemon[idxRoamer]       = true
    $PokemonGlobal.roamPokemonCaught[idxRoamer] = (decision == 4)
  end
  $PokemonGlobal.roamEncounter = nil
  $PokemonGlobal.roamedAlready = true
  $game_temp.roamer_index_for_encounter = nil
  # Used by the Poké Radar to update/break the chain
  EventHandlers.trigger(:on_wild_battle_end, species, level, decision)
  # Return false if the player lost or drew the battle, and true if any other result
  return (decision != 2 && decision != 5)
end
I'm going to modify this method to reset the weather if the Pokémon was defeated or caught - that way, the weather will be fixed as part of the process of the battle, and I won't have to do anything extra. I could just edit the method directly, but instead I'm going to copy it and put it in my new script. This will overwrite the old version, but it'll still be there if I need to refer back to it. (It's also a good habit to keep your changes separate from base Essentials, so you can find them more easily.

- I want to change the weather of a map based on its metadata

I saw how to make an on_enter_map handler, and I know that the weather is probably also changed when the player enters a map. Sure enough, a search for on_enter_map turns up this!

Ruby:
Expand Collapse Copy
# Set up various data related to the new map
EventHandlers.add(:on_enter_map, :setup_new_map,
  proc { |old_map_id|   # previous map ID, is 0 if no map ID
    # Record new Teleport destination
    new_map_metadata = $game_map.metadata
    if new_map_metadata&.teleport_destination
      $PokemonGlobal.healingSpot = new_map_metadata.teleport_destination
    end
    # End effects that apply only while on the map they were used
    $PokemonMap&.clear
    # Setup new wild encounter tables
    $PokemonEncounters&.setup($game_map.map_id)
    # Record the new map as having been visited
    $PokemonGlobal.visitedMaps[$game_map.map_id] = true
    # Set weather if new map has weather
    next if old_map_id == 0 || old_map_id == $game_map.map_id
    next if !new_map_metadata || !new_map_metadata.weather
    map_infos = pbLoadMapInfos
    if $game_map.name == map_infos[old_map_id].name
      old_map_metadata = GameData::MapMetadata.try_get(old_map_id)
      next if old_map_metadata&.weather
    end
    new_weather = new_map_metadata.weather
    $game_screen.weather(new_weather[0], 9, 0) if rand(100) < new_weather[1]
  }
)
I don't need to worry about the other setup, and I don't need to worry about checking the old map's weather. I do need to think about the chance element, though. Obviously, I want the Pokémon's weather to end - but if the map has weather that only appears with a certain chance, should I just guarantee that result? Or should I still check the chance, and just reset the weather if it isn't triggered? I think I want to keep mechanics the same, so I'm going to go with the latter, which will involve some rearranging.

Ruby:
Expand Collapse Copy
    weather = $game_map.metadata.weather
    if rand(100) < weather[1]
      $game_screen.weather(weather[0], 9, 0)
    else
      $game_screen.weather(:None, 9, 0)
    end

1654425260424.png
3 - Convert
1654425255678.png

When the player moves to a new map with a different name...
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name

  }
)

...the game checks if there's a roaming Pokémon on the map.
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
  }
)
If there is a Pokémon...
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
 
    end
  }
)
...the game should check what species it is, and get its respective weather.
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
      case species
        when :GROUDON
          weather = :Sun
        when :KYOGRE
          weather = :HeavyRain
        when :TORNADUS
          weather = :Rain
        when :THUNDURUS
          weather = :Storm
     end
    end
  }
)
Then, the game should make sure that the current map is outdoors, and that it's not about to make it sunny at night.
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
      case species
        when :GROUDON
          weather = :Sun
        when :KYOGRE
          weather = :HeavyRain
        when :TORNADUS
          weather = :Rain
        when :THUNDURUS
          weather = :Storm
      end
      if $game_map.metadata&.outdoor_map && !(PBDayNight.isNight? && weather == :Sun)
 
      end
    end
  }
)
If these conditions are met, then the map's weather should change to the new weather, if there is a new weather.
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
      case species
        when :GROUDON
          weather = :Sun
        when :KYOGRE
          weather = :HeavyRain
        when :TORNADUS
          weather = :Rain
        when :THUNDURUS
          weather = :Storm
      end
      if $game_map.metadata&.outdoor_map && !(PBDayNight.isNight? && weather == :Sun)
        $game_screen.weather(weather, 9, 20) if weather
      end
    end
  }
)
If the roaming Pokémon is caught or defeated, the map's weather should return to what it usually is based on the metadata.
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
      case species
        when :GROUDON
          weather = :Sun
        when :KYOGRE
          weather = :HeavyRain
        when :TORNADUS
          weather = :Rain
        when :THUNDURUS
          weather = :Storm
      end
      if $game_map.metadata&.outdoor_map && !(PBDayNight.isNight? && weather == :Sun)
        $game_screen.weather(weather, 9, 20) if weather
      end
    end
  }
)
def pbRoamingPokemonBattle(species, level)
  # Get the roaming Pokémon to encounter; generate it based on the species and
  # level if it doesn't already exist
  idxRoamer = $game_temp.roamer_index_for_encounter
  if !$PokemonGlobal.roamPokemon[idxRoamer] ||
     !$PokemonGlobal.roamPokemon[idxRoamer].is_a?(Pokemon)
    $PokemonGlobal.roamPokemon[idxRoamer] = pbGenerateWildPokemon(species, level, true)
  end
  # Set some battle rules
  setBattleRule("single")
  setBattleRule("roamerFlees")
  # Perform the battle
  decision = WildBattle.start_core($PokemonGlobal.roamPokemon[idxRoamer])
  # Update Roaming Pokémon data based on result of battle
  if [1, 4].include?(decision)   # Defeated or caught
    $PokemonGlobal.roamPokemon[idxRoamer]       = true
    $PokemonGlobal.roamPokemonCaught[idxRoamer] = (decision == 4)
    weather = $game_map.metadata.weather
    if rand(100) < weather[1]
      $game_screen.weather(weather[0], 9, 0)
    else
      $game_screen.weather(:None, 9, 0)
    end
  end
  $PokemonGlobal.roamEncounter = nil
  $PokemonGlobal.roamedAlready = true
  $game_temp.roamer_index_for_encounter = nil
  # Used by the Poké Radar to update/break the chain
  EventHandlers.trigger(:on_wild_battle_end, species, level, decision)
  # Return false if the player lost or drew the battle, and true if any other result
  return (decision != 2 && decision != 5)
end


1654425334918.png
4 - Playtest
1654425323368.png
I'm going to start by editing the Roaming Pokémon data in Settings to make it easier to playtest. I'm going to set it up so that Kyogre can only spawn in Lappet Town or outside of the Safari Zone. I'm not going to delete the original data, though, I'm just going to comment it out, so I can put it back later.
Ruby:
Expand Collapse Copy
    [:KYOGRE, 40, 54, 2, nil, {
      2  => [   66    ],
      66  => [   2    ],
      #2  => [   21, 31    ],
      #21 => [2,     31, 69],
      #31 => [2, 21,     69],
      #69 => [   21, 31    ]
    }],

While I'm at it, I should also remove that 25% chance of getting a roaming encounter, so I don't have to deal with a bunch of standard encounters trying to test this.
Ruby:
Expand Collapse Copy
EventHandlers.add(:on_wild_species_chosen, :roaming_pokemon,
  proc { |encounter|
    $game_temp.roamer_index_for_encounter = nil
    next if !encounter
    # Give the regular encounter if encountering a roaming Pokémon isn't possible
    next if $PokemonGlobal.roamedAlready
    next if $PokemonGlobal.partner
    next if $game_temp.poke_radar_data
    next if rand(100) < 75   # 25% chance of encountering a roaming Pokémon
I'll just comment out that next if rand(100) < 75.

Now to begin!

Right after loading my save, I get an error.
[Pokémon Essentials version 20]
[v20 Hotfixes 1.0.2]

Exception: NameError
Message: undefined local variable or method `species' for nil:NilClass

Backtrace:
385:Weather Roam:34:in `block in <main>'
035:Event_Handlers:89:in `block in trigger'
035:Event_Handlers:89:in `each_value'

It looks like I made a mistake in my check to see if a Pokémon was on the map. I thought I could just check if the variable had been defined, but that's giving me this error. So instead, I should start by defining species as a nil value - and since I run a similar check later for weather, I should do the same for it.

Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    species = nil
    weather = nil
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
      case species
        when :GROUDON
          weather = :Sun
        when :KYOGRE
          weather = :HeavyRain
        when :TORNADUS
          weather = :Rain
        when :THUNDURUS
          weather = :Storm
      end
      if $game_map.metadata&.outdoor_map && !(PBDayNight.isNight? && weather == :Sun)
        $game_screen.weather(weather, 9, 20) if weather
      end
    end
  }
)
def pbRoamingPokemonBattle(species, level)
  # Get the roaming Pokémon to encounter; generate it based on the species and
  # level if it doesn't already exist
  idxRoamer = $game_temp.roamer_index_for_encounter
  if !$PokemonGlobal.roamPokemon[idxRoamer] ||
     !$PokemonGlobal.roamPokemon[idxRoamer].is_a?(Pokemon)
    $PokemonGlobal.roamPokemon[idxRoamer] = pbGenerateWildPokemon(species, level, true)
  end
  # Set some battle rules
  setBattleRule("single")
  setBattleRule("roamerFlees")
  # Perform the battle
  decision = WildBattle.start_core($PokemonGlobal.roamPokemon[idxRoamer])
  # Update Roaming Pokémon data based on result of battle
  if [1, 4].include?(decision)   # Defeated or caught
    $PokemonGlobal.roamPokemon[idxRoamer]       = true
    $PokemonGlobal.roamPokemonCaught[idxRoamer] = (decision == 4)
    weather = $game_map.metadata.weather
    if rand(100) < weather[1]
      $game_screen.weather(weather[0], 9, 0)
    else
      $game_screen.weather(:None, 9, 0)
    end
  end
  $PokemonGlobal.roamEncounter = nil
  $PokemonGlobal.roamedAlready = true
  $game_temp.roamer_index_for_encounter = nil
  # Used by the Poké Radar to update/break the chain
  EventHandlers.trigger(:on_wild_battle_end, species, level, decision)
  # Return false if the player lost or drew the battle, and true if any other result
  return (decision != 2 && decision != 5)
end

When I turn on the switch for Kyogre to roam and switch maps, I get this error message -
[Pokémon Essentials version 20]
[v20 Hotfixes 1.0.2]

Exception: NameError
Message: undefined local variable or method `currentRegion' for nil:NilClass

Backtrace:
385:Weather Roam:29:in `block (2 levels) in <main>'
385:Weather Roam:11:in `each'
385:Weather Roam:11:in `each_with_index'

This is from that bit of code I pulled for getting the roaming Pokémon. It looks like I didn't realize currentRegion was defined earlier in that script section, so I'll go back there and see what I need.
Ruby:
Expand Collapse Copy
    currentRegion = pbGetCurrentRegion
    currentMapName = $game_map.name
Looks like currentMapName also gets used in that section, so I'll make sure to put that in there, too.

Ruby:
Expand Collapse Copy
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    species = nil
    weather = nil
    currentRegion = pbGetCurrentRegion
    currentMapName = $game_map.name
    # Get and compare map names
    mapInfos = pbLoadMapInfos

I turn the switch on for Kyogre to roam, and... it's on Route 3? That's in the original pool of locations, but it shouldn't be spawning there now, should it? I tried starting a new save file, and that seems to have done the trick. That might be something to remember for the future, if my game is one where I'll release updates.

I step outside, and there's heavy rain, and Kyogre's on the map! Seems like things have gone how I want them to! Except... when I go back inside, it's raining indoors. What gives?

I look back at the weather event on Route 7, and this tidbit is included -
Note that the weather metadata for this map is set, even though its probability is 0. This prevents the weather from lingering when you leave this map.
Well, I don't like the idea of setting weather metadata for every map in the game. (Or even just every map that connects to a map where you can find a roaming Pokémon) But maybe there's something that can be done about that?

Let's go back to the Overworld script, where we found the code for setting up the map's weather. If we search for weather, we'll also find this:
Ruby:
Expand Collapse Copy
# Clears the weather of the old map, if the old map has defined weather and the
# new map either has the same name as the old map or doesn't have defined
# weather.
EventHandlers.add(:on_leave_map, :end_weather,
  proc { |new_map_id, new_map|
    next if new_map_id == 0
    old_map_metadata = $game_map.metadata
    next if !old_map_metadata || !old_map_metadata.weather
    map_infos = pbLoadMapInfos
    if $game_map.name == map_infos[new_map_id].name
      new_map_metadata = GameData::MapMetadata.try_get(new_map_id)
      next if new_map_metadata&.weather
    end
    $game_screen.weather(:None, 0, 0)
  }
)
And there it is! This resets the weather, unless the old map doesn't have weather metadata. (or any metadata at all) So I'll just comment out next if !old_map_metadata || !old_map_metadata.weather.

I have to confess, I'm kind of cheating here - I'm already familiar with the solution here, because I had to deal with the same thing for my weather field moves script)

Moving between maps works fine now! But there's one last step - I need to make sure the weather will end if Kyogre is caught or defeated! I'm starting to regret my choice of maps - there's just a little bit of water to surf on in Lapett Town, so triggering the encounter is a bit annoying. (Good thing I definitely remember to give myself a Master Ball the first time, right? Hahaha...)

With Kyogre caught, I'm back in the overworld, and...
[2022-06-05 04:55:20 -0500]
[Pokémon Essentials version 20]
[v20 Hotfixes 1.0.2]

Exception: NoMethodError
Message: undefined method `[]' for nil:NilClass

Backtrace:
385:Weather Roam:73:in `pbRoamingPokemonBattle'
244:Overworld_RoamingPokemon:199:in `block in <main>'
035:Event_Handlers:89:in `block in trigger'
Uh oh!

Looks like I forgot that these maps don't usually have weather, so when I checked for the metadata here -
Ruby:
Expand Collapse Copy
    weather = $game_map.metadata.weather
    if rand(100) < weather[1]
      $game_screen.weather(weather[0], 9, 0)
    else
      $game_screen.weather(:None, 9, 0)
    end
It's throwing an error, because it can't get the % chance of the non-existent weather metadata. Nothing a quick check shouldn't fix!
Ruby:
Expand Collapse Copy
    weather = $game_map.metadata.weather
    if weather && rand(100) < weather[1]
      $game_screen.weather(weather[0], 9, 0)
    else
      $game_screen.weather(:None, 9, 0)
    end

And it worked! It was a bit jumpy, though, so I might consider changing that 0 to a 20 instead.

I haven't quite finished my playtesting, though - I need to make sure that the sun won't be triggered at night. I'll just switch Kyogre's weather real fast and change my computer's clock to make sure I'm good!

This example script is free to use as well, with credit!

Ruby:
Expand Collapse Copy
# When the player moves to a new map (with a different name), make all roaming
# Pokémon roam.
EventHandlers.add(:on_enter_map, :roaming_weather,
  proc { |old_map_id|
    species = nil
    weather = nil
    currentRegion = pbGetCurrentRegion
    currentMapName = $game_map.name
    # Get and compare map names
    mapInfos = pbLoadMapInfos
    next if mapInfos && old_map_id > 0 && mapInfos[old_map_id] &&
            mapInfos[old_map_id].name && $game_map.name == mapInfos[old_map_id].name
    Settings::ROAMING_SPECIES.each_with_index do |data, i|
      # data = [species, level, Game Switch, roamer method, battle BGM, area maps hash]
      next if !GameData::Species.exists?(data[0])
      next if data[2] > 0 && !$game_switches[data[2]]   # Isn't roaming
      next if $PokemonGlobal.roamPokemon[i] == true   # Roaming Pokémon has been caught
      # Get the roamer's current map
      roamerMap = $PokemonGlobal.roamPosition[i]
      if !roamerMap
        mapIDs = pbRoamingAreas(i).keys   # Hash of area patrolled by the roaming Pokémon
        next if !mapIDs || mapIDs.length == 0   # No roaming area defined somehow
        roamerMap = mapIDs[rand(mapIDs.length)]
        $PokemonGlobal.roamPosition[i] = roamerMap
      end
      # If roamer isn't on the current map, check if it's on a map with the same
      # name and in the same region
      if roamerMap != $game_map.map_id
        map_metadata = GameData::MapMetadata.try_get(roamerMap)
        next if !map_metadata || !map_metadata.town_map_position ||
                map_metadata.town_map_position[0] != currentRegion
        next if pbGetMapNameFromId(roamerMap) != currentMapName
      end
      # Get this Pokémon's species
      species = data[0]
    end
    if species
      case species
        when :GROUDON
          weather = :Sun
        when :KYOGRE
          weather = :Sun
        when :TORNADUS
          weather = :Rain
        when :THUNDURUS
          weather = :Storm
      end
      if $game_map.metadata&.outdoor_map && !(PBDayNight.isNight? && weather == :Sun)
        $game_screen.weather(weather, 9, 20) if weather
      end
    end
  }
)

def pbRoamingPokemonBattle(species, level)
  # Get the roaming Pokémon to encounter; generate it based on the species and
  # level if it doesn't already exist
  idxRoamer = $game_temp.roamer_index_for_encounter
  if !$PokemonGlobal.roamPokemon[idxRoamer] ||
     !$PokemonGlobal.roamPokemon[idxRoamer].is_a?(Pokemon)
    $PokemonGlobal.roamPokemon[idxRoamer] = pbGenerateWildPokemon(species, level, true)
  end
  # Set some battle rules
  setBattleRule("single")
  setBattleRule("roamerFlees")
  # Perform the battle
  decision = WildBattle.start_core($PokemonGlobal.roamPokemon[idxRoamer])
  # Update Roaming Pokémon data based on result of battle
  if [1, 4].include?(decision)   # Defeated or caught
    $PokemonGlobal.roamPokemon[idxRoamer]       = true
    $PokemonGlobal.roamPokemonCaught[idxRoamer] = (decision == 4)
    weather = $game_map.metadata.weather
    if weather && rand(100) < weather[1]
      $game_screen.weather(weather[0], 9, 20)
    else
      $game_screen.weather(:None, 9, 20)
    end
  end
  $PokemonGlobal.roamEncounter = nil
  $PokemonGlobal.roamedAlready = true
  $game_temp.roamer_index_for_encounter = nil
  # Used by the Poké Radar to update/break the chain
  EventHandlers.trigger(:on_wild_battle_end, species, level, decision)
  # Return false if the player lost or drew the battle, and true if any other result
  return (decision != 2 && decision != 5)
end
1696581310537.png
I'm scared to mess with the scripts...
1696581307351.png

1696581313924.png
What if I break something?
1696581316964.png

There's no reason to be afraid of trying new things in the script editor. You're not going to stumble into code that erases all of your maps, corrupts your whole project, or sets your computer on fire. It is common to get an error message that won't let you start your game - but that's because the game is reading the scripts when it starts up, and syntax errors will cause it to crash. Your game has not been irrevocably damaged, you just need to make a quick fix to get it to start again. If you can't find what you changed, just copy the script section over from a vanilla copy of Essentials. You can even copy the whole Scripts.rxdata file over if you need to.

Keep backups. Keep change logs. Keep vanilla copies of Essentials. Use text-compare, and comment keywords in your changes.

You are not going to break your game beyond repair by making changes in the script editor. That's a promise.

Relevant resources
Credits
No credits needed for the scripting tutorial. If you're using the script I created as an example, please credit TechSkylander1518 and silentgamer64.
Author
TechSkylander1518
Views
2,277
First release
Last update

Ratings

0.00 star(s) 0 ratings

More resources from TechSkylander1518

Back
Top