Pokémon R/B: Version 3 (#1520)

* Coin items received or found in the Game Corner are now shuffled, locations require Coin Case
* Prizesanity option (shuffle Game Corner Prizes)
* DexSanity option: location checks for marking Pokémon as caught in your Pokédex. Also an option to set all Pokémon in your Pokédex as seen from the start, to aid in locating them.
* Option to randomize the layout of the Rock Tunnel.
* Area 1-to-1 mapping: When one instance of a Wild Pokémon in a given area is randomized, all instances of that Pokémon will be the same. So that if a route had 3 different Pokémon before, it will have 3 after randomization.
* Option to randomize the moves taught by TMs.
* Exact controls for TM/HM compatibility chances.
* Option to randomize Pokémon's pallets or set them based on primary type.
* Added Cinnabar Gym trainers to Trainersanity and randomized the quiz questions and answers. Getting a correct answer will flag the trainer as defeated so that you can obtain the Trainersanity check without defeating the trainer if you answer correctly.
This commit is contained in:
Alchav
2023-03-13 18:40:55 -04:00
committed by GitHub
parent 4d7bd929bc
commit df55455fc0
18 changed files with 2025 additions and 918 deletions

View File

@@ -15,7 +15,7 @@ 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_static_pokemon, process_move_data
from .rules import set_rules
import worlds.pokemon_rb.poke_data as poke_data
@@ -40,13 +40,14 @@ class PokemonRedBlueWorld(World):
game = "Pokemon Red and Blue"
option_definitions = pokemon_rb_options
data_version = 5
required_client_version = (0, 3, 7)
data_version = 7
required_client_version = (0, 3, 9)
topology_present = False
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"}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
and location.address is not None}
item_name_groups = item_groups
web = PokemonWebWorld()
@@ -58,11 +59,14 @@ class PokemonRedBlueWorld(World):
self.extra_badges = {}
self.type_chart = None
self.local_poke_data = None
self.local_move_data = None
self.local_tms = None
self.learnsets = None
self.trainer_name = None
self.rival_name = None
self.type_chart = None
self.traps = None
self.trade_mons = {}
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
@@ -94,6 +98,12 @@ 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 self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2:
badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3:
@@ -107,6 +117,7 @@ class PokemonRedBlueWorld(World):
for badge in badges_to_add:
self.extra_badges[hm_moves.pop()] = badge
process_move_data(self)
process_pokemon_data(self)
if self.multiworld.randomize_type_chart[self.player] == "vanilla":
@@ -178,8 +189,13 @@ class PokemonRedBlueWorld(World):
if self.multiworld.randomize_pokedex[self.player] == "start_with":
start_inventory["Pokedex"] = 1
self.multiworld.push_precollected(self.create_item("Pokedex"))
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:
if not location.inclusion(self.multiworld, self.player):
continue
@@ -189,9 +205,18 @@ class PokemonRedBlueWorld(World):
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
location.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])
else:
item = self.create_item(location.original_item)
else:
item = self.create_item(location.original_item)
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 (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())
@@ -205,9 +230,62 @@ class PokemonRedBlueWorld(World):
self.multiworld.itempool += item_pool
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"]
for location in self.multiworld.get_locations(self.player):
if location.name in pokemon_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]
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)
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):
intervene_move = "Surf"
if not test_state.pokemon_rb_can_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"
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)
last_intervene = intervene_move
else:
break
if self.multiworld.old_man[self.player].value == 1:
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
@@ -237,17 +315,26 @@ class PokemonRedBlueWorld(World):
else:
raise FillError(f"Failed to place badges for player {self.player}")
locs = [self.multiworld.get_location("Fossil - Choice A", self.player),
self.multiworld.get_location("Fossil - Choice B", self.player)]
for loc in locs:
add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"]
or i.name == "Master Ball")
# 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.
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)]):
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.append(loc)
locs.add(loc)
for loc in locs:
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)
@@ -262,21 +349,6 @@ class PokemonRedBlueWorld(World):
unplaced_items.append(item)
self.multiworld.itempool += unplaced_items
intervene = False
test_state = self.multiworld.get_all_state(False)
if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player):
intervene = True
elif self.multiworld.accessibility[self.player].current_key != "minimal":
if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player):
intervene = True
if intervene:
# the way this is handled will be improved significantly in the future when I add options to
# let you choose the exact weights for HM compatibility
logging.warning(
f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}")
loc = self.multiworld.get_location("Route 1 - Wild Pokemon - 1", self.player)
loc.item = self.create_item("Mew")
def create_regions(self):
if self.multiworld.free_fly_location[self.player].value:
if self.multiworld.old_man[self.player].value == 0:
@@ -317,6 +389,12 @@ class PokemonRedBlueWorld(World):
spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n")
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"]
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
@@ -336,6 +414,21 @@ class PokemonRedBlueWorld(World):
self.traps += ["Ice Trap"] * self.multiworld.ice_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]:
hint_data[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):
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] = \
", ".join(mon_locations[mon])
def fill_slot_data(self) -> dict:
return {
"second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value,
@@ -358,7 +451,8 @@ class PokemonRedBlueWorld(World):
"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
"death_link": self.multiworld.death_link[self.player].value,
"prizesanity": self.multiworld.prizesanity[self.player].value
}