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.
This commit is contained in:
Alchav
2023-07-23 18:46:54 -04:00
committed by GitHub
parent cf8ac49f76
commit 85b92e2696
20 changed files with 8098 additions and 3603 deletions

View File

@@ -1,26 +1,27 @@
import os
import settings
import typing
import threading
from copy import deepcopy
from typing import TextIO
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, LocationProgressType
from Fill import fill_restrictive, FillError, sweep_from_pool
from ..AutoWorld import World, WebWorld
from ..generic.Rules import add_item_rule
from .items import item_table, item_groups
from .locations import location_data, PokemonRBLocation
from .regions import create_regions
from .logic import PokemonLogic
from .options import pokemon_rb_options
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\
process_static_pokemon, process_move_data, RedDeltaPatch, BlueDeltaPatch
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
from .pokemon import process_pokemon_data, process_move_data
from .encounters import process_pokemon_locations, process_trainer_data
from .rules import set_rules
import worlds.pokemon_rb.poke_data as poke_data
from .level_scaling import level_scaling
from . import logic
from . import poke_data
class PokemonSettings(settings.Group):
@@ -67,10 +68,10 @@ class PokemonRedBlueWorld(World):
option_definitions = pokemon_rb_options
settings: typing.ClassVar[PokemonSettings]
data_version = 8
data_version = 9
required_client_version = (0, 3, 9)
topology_present = False
topology_present = True
item_name_to_id = {name: data.id for name, data in item_table.items()}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
@@ -81,8 +82,12 @@ class PokemonRedBlueWorld(World):
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.item_pool = []
self.total_key_items = None
self.fly_map = None
self.fly_map_code = None
self.town_map_fly_map = None
self.town_map_fly_map_code = None
self.extra_badges = {}
self.type_chart = None
self.local_poke_data = None
@@ -94,6 +99,9 @@ class PokemonRedBlueWorld(World):
self.type_chart = None
self.traps = None
self.trade_mons = {}
self.finished_level_scaling = threading.Event()
self.dexsanity_table = []
self.local_locs = []
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
@@ -125,11 +133,14 @@ class PokemonRedBlueWorld(World):
if len(self.multiworld.player_name[self.player].encode()) > 16:
raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for Pokémon Red and Blue.")
if (self.multiworld.dexsanity[self.player] and self.multiworld.accessibility[self.player] == "locations"
and (self.multiworld.catch_em_all[self.player] != "all_pokemon"
or self.multiworld.randomize_wild_pokemon[self.player] == "vanilla"
or self.multiworld.randomize_legendary_pokemon[self.player] != "any")):
self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("items")
if not self.multiworld.badgesanity[self.player]:
self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"]
if self.multiworld.key_items_only[self.player]:
self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off")
self.multiworld.dexsanity[self.player] = self.multiworld.dexsanity[self.player].from_text("false")
self.multiworld.randomize_hidden_items[self.player] = \
self.multiworld.randomize_hidden_items[self.player].from_text("off")
if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2:
badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
@@ -161,9 +172,14 @@ class PokemonRedBlueWorld(World):
not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value
normals = self.multiworld.normal_matchups[self.player].value
while super_effectives + not_very_effectives + normals < 225 - immunities:
super_effectives += self.multiworld.super_effective_matchups[self.player].value
not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value
normals += self.multiworld.normal_matchups[self.player].value
if super_effectives == not_very_effectives == normals == 0:
super_effectives = 225
not_very_effectives = 225
normals = 225
else:
super_effectives += self.multiworld.super_effective_matchups[self.player].value
not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value
normals += self.multiworld.normal_matchups[self.player].value
if super_effectives + not_very_effectives + normals > 225 - immunities:
total = super_effectives + not_very_effectives + normals
excess = total - (225 - immunities)
@@ -209,143 +225,228 @@ class PokemonRedBlueWorld(World):
# damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
# to the way effectiveness messages are generated.
self.type_chart = sorted(chart, key=lambda matchup: -matchup[2])
self.multiworld.early_items[self.player]["Exp. All"] = 1
def create_items(self) -> None:
start_inventory = self.multiworld.start_inventory[self.player].value.copy()
if self.multiworld.randomize_pokedex[self.player] == "start_with":
start_inventory["Pokedex"] = 1
self.multiworld.push_precollected(self.create_item("Pokedex"))
self.dexsanity_table = [
*(True for _ in range(round(self.multiworld.dexsanity[self.player].value * 1.51))),
*(False for _ in range(151 - round(self.multiworld.dexsanity[self.player].value * 1.51)))
]
self.multiworld.random.shuffle(self.dexsanity_table)
locations = [location for location in location_data if location.type == "Item"]
item_pool = []
combined_traps = (self.multiworld.poison_trap_weight[self.player].value
+ self.multiworld.fire_trap_weight[self.player].value
+ self.multiworld.paralyze_trap_weight[self.player].value
+ self.multiworld.ice_trap_weight[self.player].value)
for location in locations:
event = location.event
if not location.inclusion(self.multiworld, self.player):
def create_items(self):
self.multiworld.itempool += self.item_pool
@classmethod
def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempool, fill_locations):
locs = []
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
locs += world.local_locs
for loc in sorted(locs):
if loc.item:
continue
if location.original_item in self.multiworld.start_inventory[self.player].value and \
location.original_item in item_groups["Unique"]:
start_inventory[location.original_item] -= 1
item = self.create_filler()
elif location.original_item is None:
item = self.create_filler()
elif location.original_item == "Pokedex":
if self.multiworld.randomize_pokedex[self.player] == "vanilla":
self.multiworld.get_location(location.name, self.player).event = True
event = True
item = self.create_item("Pokedex")
elif location.original_item.startswith("TM"):
if self.multiworld.randomize_tm_moves[self.player]:
item = self.create_item(location.original_item.split(" ")[0])
itempool = progitempool + usefulitempool + filleritempool
multiworld.random.shuffle(itempool)
unplaced_items = []
for item in itempool:
if item.player == loc.player and loc.can_fill(multiworld.state, item, False):
if item in progitempool:
progitempool.remove(item)
elif item in usefulitempool:
usefulitempool.remove(item)
elif item in filleritempool:
filleritempool.remove(item)
if item.advancement:
state = sweep_from_pool(multiworld.state, progitempool + unplaced_items)
if (not item.advancement) or state.can_reach(loc, "Location", loc.player):
multiworld.push_item(loc, item, False)
loc.event = item.advancement
fill_locations.remove(loc)
break
else:
unplaced_items.append(item)
progitempool += [item for item in unplaced_items if item.advancement]
usefulitempool += [item for item in unplaced_items if item.useful]
filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)]
def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
if not self.multiworld.badgesanity[self.player]:
# Door Shuffle options besides Simple place badges during door shuffling
if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"):
badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
for badge in badges:
self.multiworld.itempool.remove(badge)
progitempool.remove(badge)
for _ in range(5):
badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
"Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]]
state = self.multiworld.get_all_state(False)
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()
# allow_partial so that unplaced badges aren't lost, for debugging purposes
fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True)
if badges:
for location in badgelocs:
if location.item:
badges.append(location.item)
location.item = None
continue
else:
for location in badgelocs:
if location.item:
fill_locations.remove(location)
break
else:
item = self.create_item(location.original_item)
else:
item = self.create_item(location.original_item)
if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100)
<= self.multiworld.trap_percentage[self.player].value and combined_traps != 0):
item = self.create_item(self.select_trap())
if event:
self.multiworld.get_location(location.name, self.player).place_locked_item(item)
elif "Badge" not in item.name or self.multiworld.badgesanity[self.player].value:
item_pool.append(item)
raise FillError(f"Failed to place badges for player {self.player}")
self.multiworld.random.shuffle(item_pool)
if self.multiworld.key_items_only[self.player]:
return
self.multiworld.itempool += item_pool
tms = [item for item in usefulitempool + filleritempool if item.name.startswith("TM") and (item.player ==
self.player or (item.player in self.multiworld.groups and self.player in
self.multiworld.groups[item.player]["players"]))]
if len(tms) > 7:
for gym_leader in (("Pewter Gym", "Brock"), ("Cerulean Gym", "Misty"), ("Vermilion Gym", "Lt. Surge"),
("Celadon Gym-C", "Erika"), ("Fuchsia Gym", "Koga"), ("Saffron Gym-C", "Sabrina"),
("Cinnabar Gym", "Blaine"), ("Viridian Gym", "Giovanni")):
loc = self.multiworld.get_location(f"{gym_leader[0].split('-')[0]} - {gym_leader[1]} TM",
self.player)
if loc.item:
continue
for party in self.multiworld.get_location(gym_leader[0] + " - Trainer Parties", self.player).party_data:
if party["party_address"] == \
f"Trainer_Party_{gym_leader[1].replace('. ', '').replace('Giovanni', 'Viridian_Gym_Giovanni')}_A":
mon = party["party"][-1]
learnable_tms = [tm for tm in tms if self.local_poke_data[mon]["tms"][
int((int(tm.name[2:4]) - 1) / 8)] & 1 << ((int(tm.name[2:4]) - 1) % 8)]
if not learnable_tms:
learnable_tms = tms
tm = self.multiworld.random.choice(learnable_tms)
loc.place_locked_item(tm)
fill_locations.remove(loc)
tms.remove(tm)
if tm.useful:
usefulitempool.remove(tm)
else:
filleritempool.remove(tm)
break
else:
raise Exception("Missing Gym Leader data")
def pre_fill(self) -> None:
process_wild_pokemon(self)
process_static_pokemon(self)
pokemon_locs = [location.name for location in location_data if location.type != "Item"]
process_pokemon_locations(self)
process_trainer_data(self)
locs = [location.name for location in location_data if location.type != "Item"]
for location in self.multiworld.get_locations(self.player):
if location.name in pokemon_locs:
location.show_in_spoiler = False
if location.name in locs:
location.show_in_spoiler = False
def intervene(move):
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if loc.type == "Wild Encounter"]
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
placed_mons = [slot.item.name for slot in accessible_slots]
# this sort method doesn't seem to work if you reference the same list being sorted in the lambda
placed_mons_copy = placed_mons.copy()
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
if self.multiworld.area_1_to_1_mapping[self.player]:
zone = " - ".join(placed_mon.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name ==
placed_mon]
def intervene(move, test_state):
if self.multiworld.randomize_wild_pokemon[self.player]:
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
loc.type == "Wild Encounter"]
def number_of_zones(mon):
zones = set()
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
zones.add(loc.name.split(" - ")[0])
return len(zones)
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
placed_mons = [slot.item.name for slot in accessible_slots]
if self.multiworld.area_1_to_1_mapping[self.player]:
placed_mons.sort(key=lambda i: number_of_zones(i))
else:
# this sort method doesn't work if you reference the same list being sorted in the lambda
placed_mons_copy = placed_mons.copy()
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
replace_mon = self.multiworld.random.choice(viable_mons)
replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
== placed_mon])
if self.multiworld.area_1_to_1_mapping[self.player]:
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
== placed_mon]
for replace_slot in replace_slots:
replace_slot.item = self.create_item(replace_mon)
else:
replace_slot.item = self.create_item(replace_mon)
else:
replace_slots = [self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name ==
placed_mon])]
replace_mon = self.multiworld.random.choice(viable_mons)
for replace_slot in replace_slots:
replace_slot.item = self.create_item(replace_mon)
tms_hms = self.local_tms + poke_data.hm_moves
flag = tms_hms.index(move)
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)]
self.multiworld.random.shuffle(mon_list)
mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in
[self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]])
for mon in mon_list:
if test_state.has(mon, self.player):
self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
break
last_intervene = None
while True:
intervene_move = None
test_state = self.multiworld.get_all_state(False)
if not self.multiworld.badgesanity[self.player]:
for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge",
"Marsh Badge", "Volcano Badge", "Earth Badge"]:
test_state.collect(self.create_item(badge))
if not test_state.pokemon_rb_can_surf(self.player):
if not logic.can_learn_hm(test_state, "Surf", self.player):
intervene_move = "Surf"
if not test_state.pokemon_rb_can_strength(self.player):
elif not logic.can_learn_hm(test_state, "Strength", self.player):
intervene_move = "Strength"
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
# as you will require cut to access celadon gyn
if (self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_condition[self.player],
self.multiworld.victory_road_condition[self.player]) > 7)):
if not test_state.pokemon_rb_can_cut(self.player):
intervene_move = "Cut"
if (self.multiworld.accessibility[self.player].current_key != "minimal" and
(self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])):
if not test_state.pokemon_rb_can_flash(self.player):
intervene_move = "Flash"
elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and
(self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(
self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player],
self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player]
and (((self.multiworld.accessibility[self.player] != "minimal" and
(self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or
self.multiworld.door_shuffle[self.player]))):
intervene_move = "Flash"
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
# considered in door shuffle.
elif ((not logic.can_learn_hm(test_state, "Fly", self.player)) and logic.can_learn_hm(test_state, "Fly", self.player)
and self.multiworld.door_shuffle[self.player] not in
("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
intervene_move = "Fly"
if intervene_move:
if intervene_move == last_intervene:
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}")
intervene(intervene_move)
intervene(intervene_move, test_state)
last_intervene = intervene_move
else:
break
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
# fail. Re-use test_state from previous final loop.
evolutions_region = self.multiworld.get_region("Evolution", self.player)
clear_cache = False
for location in evolutions_region.locations.copy():
if not test_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
clear_cache = True
if clear_cache:
self.multiworld.clear_location_cache()
if self.multiworld.old_man[self.player] == "early_parcel":
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
if self.multiworld.dexsanity[self.player]:
for location in [self.multiworld.get_location(f"Pokedex - {mon}", self.player)
for mon in poke_data.pokemon_data.keys()]:
add_item_rule(location, lambda item: item.name != "Oak's Parcel" or item.player != self.player)
if not self.multiworld.badgesanity[self.player].value:
self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"]
for i in range(5):
try:
badges = []
badgelocs = []
for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge",
"Marsh Badge", "Volcano Badge", "Earth Badge"]:
badges.append(self.create_item(badge))
for loc in ["Pewter Gym - Brock 1", "Cerulean Gym - Misty 1", "Vermilion Gym - Lt. Surge 1",
"Celadon Gym - Erika 1", "Fuchsia Gym - Koga 1", "Saffron Gym - Sabrina 1",
"Cinnabar Gym - Blaine 1", "Viridian Gym - Giovanni 1"]:
badgelocs.append(self.multiworld.get_location(loc, self.player))
state = self.multiworld.get_all_state(False)
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
fill_restrictive(self.multiworld, state, badgelocs.copy(), badges, True, True)
except FillError:
for location in badgelocs:
location.item = None
continue
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
for i, mon in enumerate(poke_data.pokemon_data):
if self.dexsanity_table[i]:
location = self.multiworld.get_location(f"Pokedex - {mon}", self.player)
add_item_rule(location, lambda item: item.name != "Oak's Parcel" or item.player != self.player)
# Place local items in some locations to prevent save-scumming. Also Oak's PC to prevent an "AP Item" from
# entering the player's inventory.
@@ -353,52 +454,144 @@ class PokemonRedBlueWorld(World):
locs = {self.multiworld.get_location("Fossil - Choice A", self.player),
self.multiworld.get_location("Fossil - Choice B", self.player)}
if self.multiworld.dexsanity[self.player]:
for mon in ([" ".join(self.multiworld.get_location(
f"Pallet Town - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
+ [" ".join(self.multiworld.get_location(
f"Fighting Dojo - Gift {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 3)]):
for loc in locs:
if self.multiworld.fossil_check_item_types[self.player] == "key_items":
add_item_rule(loc, lambda i: i.advancement)
elif self.multiworld.fossil_check_item_types[self.player] == "unique_items":
add_item_rule(loc, lambda i: i.name in item_groups["Unique"])
elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items":
add_item_rule(loc, lambda i: not i.advancement)
for mon in ([" ".join(self.multiworld.get_location(
f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
+ [" ".join(self.multiworld.get_location(
f"Saffron Fighting Dojo - Gift {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 3)]
+ ["Vaporeon", "Jolteon", "Flareon"]):
if self.dexsanity_table[poke_data.pokemon_dex[mon] - 1]:
loc = self.multiworld.get_location(f"Pokedex - {mon}", self.player)
if loc.item is None:
locs.add(loc)
loc = self.multiworld.get_location("Pallet Town - Player's PC", self.player)
if loc.item is None:
locs.add(loc)
if not self.multiworld.key_items_only[self.player]:
loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
if loc.item is None:
locs.add(loc)
for loc in sorted(locs):
unplaced_items = []
if loc.name in self.multiworld.priority_locations[self.player].value:
add_item_rule(loc, lambda i: i.advancement)
for item in reversed(self.multiworld.itempool):
if item.player == self.player and loc.can_fill(self.multiworld.state, item, False):
self.multiworld.itempool.remove(item)
if item.advancement:
state = sweep_from_pool(self.multiworld.state, self.multiworld.itempool + unplaced_items)
if (not item.advancement) or state.can_reach(loc, "Location", self.player):
loc.place_locked_item(item)
break
add_item_rule(loc, lambda i: i.player == self.player)
if self.multiworld.old_man[self.player] == "early_parcel" and loc.name != "Player's House 2F - Player's PC":
add_item_rule(loc, lambda i: i.name != "Oak's Parcel")
self.local_locs = locs
all_state = self.multiworld.get_all_state(False)
reachable_mons = set()
for mon in poke_data.pokemon_data:
if all_state.has(mon, self.player) or all_state.has(f"Static {mon}", self.player):
reachable_mons.add(mon)
# The large number of wild Pokemon can make sweeping for events time-consuming, and is especially bad in
# the spoiler playthrough calculation because it removes each advancement item one at a time to verify
# if the game is beatable without it. We go through each zone and flag any duplicates as useful.
# Especially with area 1-to-1 mapping / vanilla wild Pokémon, this should cut down significantly on wasted time.
for region in self.multiworld.get_regions(self.player):
region_mons = set()
for location in region.locations:
if "Wild Pokemon" in location.name:
if location.item.name in region_mons:
location.item.classification = ItemClassification.useful
else:
unplaced_items.append(item)
self.multiworld.itempool += unplaced_items
region_mons.add(location.item.name)
self.multiworld.elite_four_pokedex_condition[self.player].total = \
int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value)
if self.multiworld.accessibility[self.player] == "locations":
balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]]
traps = [self.create_item(trap) for trap in item_groups["Traps"]]
locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in
location.name]
pokedex = self.multiworld.get_region("Pokedex", self.player)
remove_items = 0
for location in locations:
if not location.can_reach(all_state):
pokedex.locations.remove(location)
self.dexsanity_table[poke_data.pokemon_dex[location.name.split(" - ")[1]] - 1] = False
remove_items += 1
for _ in range(remove_items - 5):
balls.append(balls.pop(0))
for ball in balls:
try:
self.multiworld.itempool.remove(ball)
except ValueError:
continue
else:
break
else:
self.multiworld.random.shuffle(traps)
for trap in traps:
try:
self.multiworld.itempool.remove(trap)
except ValueError:
continue
else:
break
else:
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
self.multiworld._recache()
if self.multiworld.door_shuffle[self.player] == "decoupled":
swept_state = self.multiworld.state.copy()
swept_state.sweep_for_events(player=self.player)
locations = [location for location in
self.multiworld.get_reachable_locations(swept_state, self.player) if location.item is
None]
self.multiworld.random.shuffle(locations)
while len(locations) > 10:
location = locations.pop()
location.progress_type = LocationProgressType.EXCLUDED
if self.multiworld.key_items_only[self.player]:
locations = [location for location in self.multiworld.get_unfilled_locations(self.player) if
location.progress_type == LocationProgressType.DEFAULT]
for location in locations:
location.progress_type = LocationProgressType.PRIORITY
def create_regions(self):
if self.multiworld.free_fly_location[self.player].value:
if self.multiworld.old_man[self.player].value == 0:
fly_map_code = self.multiworld.random.randint(1, 9)
else:
fly_map_code = self.multiworld.random.randint(5, 9)
if fly_map_code == 5:
fly_map_code = 4
if fly_map_code == 9:
fly_map_code = 10
if (self.multiworld.old_man[self.player] == "vanilla" or
self.multiworld.door_shuffle[self.player] in ("full", "insanity")):
fly_map_codes = self.multiworld.random.sample(range(2, 11), 2)
elif (self.multiworld.door_shuffle[self.player] == "simple" or
self.multiworld.route_3_condition[self.player] == "boulder_badge" or
(self.multiworld.route_3_condition[self.player] == "any_badge" and
self.multiworld.badgesanity[self.player])):
fly_map_codes = self.multiworld.random.sample(range(3, 11), 2)
else:
fly_map_codes = self.multiworld.random.sample([4, 6, 7, 8, 9, 10], 2)
if self.multiworld.free_fly_location[self.player]:
fly_map_code = fly_map_codes[0]
else:
fly_map_code = 0
self.fly_map = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town",
"Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau",
"Saffron City"][fly_map_code]
if self.multiworld.town_map_fly_location[self.player]:
town_map_fly_map_code = fly_map_codes[1]
else:
town_map_fly_map_code = 0
fly_maps = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town",
"Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau",
"Saffron City"]
self.fly_map = fly_maps[fly_map_code]
self.town_map_fly_map = fly_maps[town_map_fly_map_code]
self.fly_map_code = fly_map_code
create_regions(self.multiworld, self.player)
self.town_map_fly_map_code = town_map_fly_map_code
create_regions(self)
self.multiworld.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player)
def set_rules(self):
@@ -407,12 +600,21 @@ class PokemonRedBlueWorld(World):
def create_item(self, name: str) -> Item:
return PokemonRBItem(name, self.player)
@classmethod
def stage_generate_output(cls, multiworld, output_directory):
level_scaling(multiworld)
def generate_output(self, output_directory: str):
generate_output(self, output_directory)
def write_spoiler_header(self, spoiler_handle: TextIO):
if self.multiworld.free_fly_location[self.player].value:
spoiler_handle.write('Fly unlocks: %s\n' % self.fly_map)
spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n")
spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n")
spoiler_handle.write(f"Elite Four Total Pokemon: {self.multiworld.elite_four_pokedex_condition[self.player].total}\n")
if self.multiworld.free_fly_location[self.player]:
spoiler_handle.write(f"Free Fly Location: {self.fly_map}\n")
if self.multiworld.town_map_fly_location[self.player]:
spoiler_handle.write(f"Town Map Fly Location: {self.town_map_fly_map}\n")
if self.extra_badges:
for hm_move, badge in self.extra_badges.items():
spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n")
@@ -423,20 +625,30 @@ class PokemonRedBlueWorld(World):
for matchup in self.type_chart:
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
spoiler_handle.write(f"\n\nPokémon locations ({self.multiworld.player_name[self.player]}):\n\n")
pokemon_locs = [location.name for location in location_data if location.type != "Item"]
pokemon_locs = [location.name for location in location_data if location.type not in ("Item", "Trainer Parties")]
for location in self.multiworld.get_locations(self.player):
if location.name in pokemon_locs:
spoiler_handle.write(location.name + ": " + location.item.name + "\n")
def get_filler_item_name(self) -> str:
combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value
if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0:
combined_traps = (self.multiworld.poison_trap_weight[self.player].value
+ self.multiworld.fire_trap_weight[self.player].value
+ self.multiworld.paralyze_trap_weight[self.player].value
+ self.multiworld.ice_trap_weight[self.player].value
+ self.multiworld.sleep_trap_weight[self.player].value)
if (combined_traps > 0 and
self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value):
return self.select_trap()
return self.multiworld.random.choice([item for item in item_table if item_table[
item].classification == ItemClassification.filler and item not in item_groups["Vending Machine Drinks"] +
item_groups["Unique"]])
banned_items = item_groups["Unique"]
if (((not self.multiworld.tea[self.player]) or "Saffron City" not in [self.fly_map, self.town_map_fly_map])
and (not self.multiworld.door_shuffle[self.player])):
# under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being
# able to reach the Celadon Department Store, so Poké Dolls would not allow early access to anything
banned_items.append("Poke Doll")
if not self.multiworld.tea[self.player]:
banned_items += item_groups["Vending Machine Drinks"]
return self.multiworld.random.choice([item for item in item_table if item_table[item].id and item_table[
item].classification == ItemClassification.filler and item not in banned_items])
def select_trap(self):
if self.traps is None:
@@ -445,23 +657,30 @@ class PokemonRedBlueWorld(World):
self.traps += ["Fire Trap"] * self.multiworld.fire_trap_weight[self.player].value
self.traps += ["Paralyze Trap"] * self.multiworld.paralyze_trap_weight[self.player].value
self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value
self.traps += ["Sleep Trap"] * self.multiworld.sleep_trap_weight[self.player].value
return self.multiworld.random.choice(self.traps)
def extend_hint_information(self, hint_data):
if self.multiworld.dexsanity[self.player]:
if self.multiworld.dexsanity[self.player] or self.multiworld.door_shuffle[self.player]:
hint_data[self.player] = {}
if self.multiworld.dexsanity[self.player]:
mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()}
for loc in location_data: #self.multiworld.get_locations(self.player):
for loc in location_data:
if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]:
mon = self.multiworld.get_location(loc.name, self.player).item.name
if mon.startswith("Static ") or mon.startswith("Missable "):
mon = " ".join(mon.split(" ")[1:])
mon_locations[mon].add(loc.name.split(" -")[0])
for mon in mon_locations:
if mon_locations[mon]:
hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] = \
for i, mon in enumerate(mon_locations):
if self.dexsanity_table[i] and mon_locations[mon]:
hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] =\
", ".join(mon_locations[mon])
if self.multiworld.door_shuffle[self.player]:
for location in self.multiworld.get_locations(self.player):
if location.parent_region.entrance_hint and location.address:
hint_data[self.player][location.address] = location.parent_region.entrance_hint
def fill_slot_data(self) -> dict:
return {
"second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value,
@@ -475,17 +694,25 @@ class PokemonRedBlueWorld(World):
"extra_strength_boulders": self.multiworld.extra_strength_boulders[self.player].value,
"tea": self.multiworld.tea[self.player].value,
"old_man": self.multiworld.old_man[self.player].value,
"elite_four_condition": self.multiworld.elite_four_condition[self.player].value,
"elite_four_badges_condition": self.multiworld.elite_four_badges_condition[self.player].value,
"elite_four_key_items_condition": self.multiworld.elite_four_key_items_condition[self.player].total,
"elite_four_pokedex_condition": self.multiworld.elite_four_pokedex_condition[self.player].total,
"victory_road_condition": self.multiworld.victory_road_condition[self.player].value,
"route_22_gate_condition": self.multiworld.route_22_gate_condition[self.player].value,
"route_3_condition": self.multiworld.route_3_condition[self.player].value,
"robbed_house_officer": self.multiworld.robbed_house_officer[self.player].value,
"viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value,
"cerulean_cave_condition": self.multiworld.cerulean_cave_condition[self.player].value,
"cerulean_cave_badges_condition": self.multiworld.cerulean_cave_badges_condition[self.player].value,
"cerulean_cave_key_items_condition": self.multiworld.cerulean_cave_key_items_condition[self.player].total,
"free_fly_map": self.fly_map_code,
"town_map_fly_map": self.town_map_fly_map_code,
"extra_badges": self.extra_badges,
"type_chart": self.type_chart,
"randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value,
"trainersanity": self.multiworld.trainersanity[self.player].value,
"death_link": self.multiworld.death_link[self.player].value,
"prizesanity": self.multiworld.prizesanity[self.player].value
"prizesanity": self.multiworld.prizesanity[self.player].value,
"key_items_only": self.multiworld.key_items_only[self.player].value,
}
@@ -499,4 +726,4 @@ class PokemonRBItem(Item):
name,
item_data.classification,
item_data.id, player
)
)