Archipelago/worlds/pokemon_rb/level_scaling.py

139 lines
8.3 KiB
Python
Raw Normal View History

Pokémon Red and Blue: Version 4 update (#1963) ## What is this fixing or adding? Adds a large number of new options, including: - Door Shuffle - Sphere-based level scaling - Key Item and Pokedex requirement options to reach the Elite Four - Split Card Key option - Dexsanity option can be set to a percentage of Pokémon that will be checks - Stonesanity: remove the stones from the Celadon Department Store and shuffle them into the item pool, replacing 4 of the 5 Moon Stone items - Sleep Trap items option - Randomize Move Types option - Town Map Fly Location option, to unlock a flight location when finding/receiving the Town Map Many enhancements have been made, including: - Game allows you to continue your save file _from Pallet Town_ as a way to save warp back to the beginning of the game. The one-way drop from Diglett's Cave to north Route 2 that had been added to the randomizer has been removed. - Client auto-hints some locations when you are able to see the item before you can obtain it (but would only show AP Item if it is for another player), including Bike Shop, Oak's Aides, Celadon Prize Corner, and the unchosen Fossil location. Various bugs have been fixed, including: - Route 13 wild Pokémon not correctly logically requiring Cut - Vanilla tm/hm compatibility options giving compatibility for many TMs/HMs erroneously - If an item that exists in multiple quantities in the item pool is chosen for one of the locations that are pre-filled with local items, it will continue placing that same item in the remaining locations as long as more of that item exist - `start_with` option for `randomize_pokedex` still shuffling a Pokédex into the item pool - The obedience threshold levels being incorrect with 0-2 badges, with Pokémon up to level 30 obeying with 0-1 badges and up to 10 with 2 badges - Receiving a DeathLink trigger in the Safari Zone causing issues. Now, you will have your steps remaining set to 0 instead of blacking out when you're in the Safari Zone. Many location names have been changed, as location names are automatically prepended using the Region name and a large number of areas have been split into new regions as part of the overhaul to add Door Shuffle.
2023-07-23 22:46:54 +00:00
from BaseClasses import CollectionState
from .locations import level_name_list, level_list
def level_scaling(multiworld):
state = CollectionState(multiworld)
locations = set(multiworld.get_filled_locations())
spheres = []
while locations:
sphere = set()
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
if multiworld.level_scaling[world.player] != "by_spheres_and_distance":
continue
regions = {multiworld.get_region("Menu", world.player)}
checked_regions = set()
distance = 0
while regions:
next_regions = set()
for region in regions:
if not getattr(region, "distance"):
region.distance = distance
next_regions.update({e.connected_region for e in region.exits if e.connected_region not in
checked_regions and e.access_rule(state)})
checked_regions.update(regions)
regions = next_regions
distance += 1
distances = {}
for location in locations:
def reachable():
if location.can_reach(state):
return True
if location.parent_region.name == "Fossil" and state.can_reach("Mt Moon B2F", "Region",
location.player):
# We want areas that are accessible earlier to have lower levels. If an important item is at a
# fossil location, it may not be in logic until much later, despite your ability to potentially
# reach them earlier. We treat them both as reachable right away for this purpose
return True
if (location.name == "Route 25 - Item" and state.can_reach("Route 25", "Region", location.player)
and multiworld.blind_trainers[location.player].value < 100):
# Assume they will take their one chance to get the trainer to walk out of the way to reach
# the item behind them
return True
if (("Rock Tunnel 1F - Wild Pokemon" in location.name
and any([multiworld.get_entrance(e, location.player).connected_region.can_reach(state)
for e in ['Rock Tunnel 1F-NE to Route 10-N',
'Rock Tunnel 1F-NE to Rock Tunnel B1F-E',
'Rock Tunnel 1F-NW to Rock Tunnel B1F-E',
'Rock Tunnel 1F-NW to Rock Tunnel B1F-W',
'Rock Tunnel 1F-S to Route 10-S',
'Rock Tunnel 1F-S to Rock Tunnel B1F-W']])) or
("Rock Tunnel B1F - Wild Pokemon" in location.name and
any([multiworld.get_entrance(e, location.player).connected_region.can_reach(state)
for e in ['Rock Tunnel B1F-E to Rock Tunnel 1F-NE',
'Rock Tunnel B1F-E to Rock Tunnel 1F-NW',
'Rock Tunnel B1F-W to Rock Tunnel 1F-NW',
'Rock Tunnel B1F-W to Rock Tunnel 1F-S']]))):
# Even if checks in Rock Tunnel are out of logic due to lack of Flash, it is very easy to
# wander in the dark and encounter wild Pokémon, even unintentionally while attempting to
# leave the way you entered. We'll count the wild Pokémon as reachable as soon as the Rock
# Tunnel is reachable, so you don't have an opportunity to catch high level Pokémon early.
# If the connections between Rock Tunnel floors are vanilla, you will still potentially
# have very high level Pokémon in B1F if you reach it out of logic, but that would always
# mean intentionally breaking the logic you picked in your yaml, and may require
# defeating trainers in 1F that would be at the higher levels.
return True
return False
if reachable():
sphere.add(location)
parent_region = location.parent_region
if getattr(parent_region, "distance", None) is None:
distance = 0
else:
distance = parent_region.distance
if distance not in distances:
distances[distance] = {location}
else:
distances[distance].add(location)
if sphere:
for distance in sorted(distances.keys()):
spheres.append(distances[distance])
locations -= distances[distance]
else:
spheres.append(locations)
break
for location in sphere:
if not location.item:
continue
if (location.item.game == "Pokemon Red and Blue" and (location.item.name.startswith("Missable ") or
location.item.name.startswith("Static ")) and location.name !=
"Pokemon Tower 6F - Restless Soul"):
# Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic static Pokemon
# are not considered for moves or evolutions, as you could release them and potentially soft lock
# the game. However, for level scaling purposes, we will treat them as not missable or static.
# We would not want someone playing a minimal accessibility Dexsanity game to get what would be
# technically an "out of logic" Mansion Key from selecting Bulbasaur at the beginning of the game
# and end up in the Mansion early and encountering level 67 Pokémon
state.collect(multiworld.worlds[location.item.player].create_item(
location.item.name.split("Missable ")[-1].split("Static ")[-1]), True, location)
else:
state.collect(location.item, True, location)
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
if multiworld.level_scaling[world.player] == "off":
continue
level_list_copy = level_list.copy()
for sphere in spheres:
sphere_objects = {loc.name: loc for loc in sphere if loc.player == world.player
and (loc.type == "Wild Encounter" or "Pokemon" in loc.type) and loc.level is not None}
party_objects = [loc for loc in sphere if loc.player == world.player and loc.type == "Trainer Parties"]
for parties in party_objects:
for party in parties.party_data:
if isinstance(party["level"], int):
sphere_objects[(party["party_address"][0] if isinstance(party["party_address"], list)
else party["party_address"], 0)] = parties
else:
for i, level in enumerate(party["level"]):
sphere_objects[(party["party_address"][0] if isinstance(party["party_address"], list)
else party["party_address"], i)] = parties
ordered_sphere_objects = list(sphere_objects.keys())
ordered_sphere_objects.sort(key=lambda obj: level_name_list.index(obj))
for object in ordered_sphere_objects:
if sphere_objects[object].type == "Trainer Parties":
for party in sphere_objects[object].party_data:
if (isinstance(party["party_address"], list) and party["party_address"][0] == object[0]) or party["party_address"] == object[0]:
if isinstance(party["level"], int):
party["level"] = level_list_copy.pop(0)
else:
party["level"][object[1]] = level_list_copy.pop(0)
break
else:
sphere_objects[object].level = level_list_copy.pop(0)
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
world.finished_level_scaling.set()