Files
dockipelago/worlds/tloz_oos/World.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

335 lines
16 KiB
Python

import os
from threading import Event
from typing import ClassVar, Any, Optional, Type, TextIO
from BaseClasses import Item, ItemClassification, MultiWorld, CollectionState
from Options import Option
from worlds.AutoWorld import World
from .Options import *
from .Settings import OracleOfSeasonsSettings
from .WebWorld import OracleOfSeasonsWeb
from .common.Util import build_location_name_to_id_dict, build_item_name_to_id_dict
from .data import LOCATIONS_DATA
from .data.Constants import *
from .data.Items import ITEMS_DATA
from .generation.Hints import create_region_hints, create_item_hints
class OracleOfSeasonsWorld(World):
"""
The Legend of Zelda: Oracles of Seasons is one of the rare Capcom entries to the series.
The seasons in the world of Holodrum have been a mess since Onox captured Din, the Oracle of Seasons.
Gather the Essences of Nature, confront Onox and rescue Din to give nature some rest in Holodrum.
"""
game = "The Legend of Zelda - Oracle of Seasons"
options_dataclass = OracleOfSeasonsOptions
options: OracleOfSeasonsOptions
web = OracleOfSeasonsWeb()
topology_present = True
settings: ClassVar[OracleOfSeasonsSettings]
settings_key = "tloz_oos_options"
location_name_to_id = build_location_name_to_id_dict(LOCATIONS_DATA)
item_name_to_id = build_item_name_to_id_dict(ITEMS_DATA)
item_name_groups = ITEM_GROUPS
location_name_groups = LOCATION_GROUPS
origin_region_name = "impa's house"
item_mapping: ClassVar[dict[str, str]] = {
"Rupees (1)": "Rupees",
"Rupees (5)": "Rupees",
"Rupees (10)": "Rupees",
"Rupees (20)": "Rupees",
"Rupees (30)": "Rupees",
"Rupees (50)": "Rupees",
"Rupees (100)": "Rupees",
"Rupees (200)": "Rupees",
"_reached_d2_rupee_room": "Rupees",
"_reached_d6_rupee_room": "Rupees",
"rupees from old man in goron mountain": "Rupees",
"rupees from old man near blaino": "Rupees",
"rupees from old man near d1": "Rupees",
"rupees from old man near western coast house": "Rupees",
"rupees from old man in horon": "Rupees",
"rupees from old man near d6": "Rupees",
"rupees from old man near holly's house": "Rupees",
"rupees from old man near mrs. ruul": "Rupees",
"Ore Chunks (10)": "Ore Chunks",
"Ore Chunks (25)": "Ore Chunks",
"Ore Chunks (50)": "Ore Chunks",
"Bombs (10)": "Bombs",
"Bombs (20)": "Bombs",
"Bombchus (10)": "Bombchus",
"Bombchus (20)": "Bombchus",
}
@classmethod
def version(cls) -> str:
return cls.world_version.as_simple_string()
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.pre_fill_items: list[Item] = []
self.default_seasons: dict[str, str] = DEFAULT_SEASONS.copy()
self.dungeon_entrances: dict[str, str] = DUNGEON_CONNECTIONS.copy()
self.portal_connections: dict[str, str] = PORTAL_CONNECTIONS.copy()
self.lost_woods_item_sequence: list[list] = LOST_WOODS_ITEM_SEQUENCE.copy()
self.lost_woods_main_sequence: list[list] = LOST_WOODS_MAIN_SEQUENCE.copy()
self.old_man_rupee_values: dict[str, int] = OLD_MAN_RUPEE_VALUES.copy()
self.samasa_gate_code: list[int] = SAMASA_GATE_CODE.copy()
self.shop_prices: dict[str, int] = VANILLA_SHOP_PRICES.copy()
self.shop_order: list[list[str]] = []
self.shop_rupee_requirements: dict[str, int] = {}
self.essences_in_game: list[str] = ITEM_GROUPS["Essences"].copy()
self.random_rings_pool: list[str] = []
self.remaining_progressive_gasha_seeds = 0
self.item_mapping_collect: dict[str, tuple[str, int]] = {}
self.made_hints = Event()
self.region_hints: list[tuple[str, str | int]] = []
self.item_hints: list[Item | None] = []
def generate_early(self) -> None:
if self.interpret_slot_data(None):
return
from .generation.GenerateEarly import generate_early
generate_early(self)
def create_regions(self) -> None:
from .generation.CreateRegions import create_regions
create_regions(self)
def set_rules(self) -> None:
from .generation.Logic import create_connections, apply_self_locking_rules
create_connections(self, self.origin_region_name, self.options)
apply_self_locking_rules(self.multiworld, self.player)
self.multiworld.completion_condition[self.player] = lambda state: state.has("_beaten_game", self.player)
self.multiworld.register_indirect_condition(self.get_region("lost woods top statue"), self.get_entrance("lost woods -> lost woods deku"))
self.multiworld.register_indirect_condition(self.get_region("lost woods phonograph"), self.get_entrance("lost woods stump -> lost woods"))
self.multiworld.register_indirect_condition(self.get_region("lost woods phonograph"), self.get_entrance("d6 sector -> lost woods"))
self.multiworld.register_indirect_condition(self.get_region("lost woods deku"), self.get_entrance("lost woods -> d6 sector"))
self.multiworld.register_indirect_condition(self.get_region("lost woods deku"), self.get_entrance("lost woods stump -> d6 sector"))
if self.options.linked_heros_cave:
for i in range(1, 9):
self.multiworld.register_indirect_condition(self.get_region(f"enter d{i}"), self.get_entrance("d11 floor 4 chest -> d11 final chest"))
if self.options.logic_difficulty == OracleOfSeasonsLogicDifficulty.option_hell:
cucco_region = self.get_region("rooster adventure")
# This saves using an event which is slightly more efficient
self.multiworld.register_indirect_condition(cucco_region, self.get_entrance("d6 sector -> old man near d6"))
self.multiworld.register_indirect_condition(cucco_region, self.get_entrance("d6 sector -> d6 entrance"))
self.multiworld.register_indirect_condition(self.get_region("lost woods top statue"), self.get_entrance("rooster adventure -> lost woods deku"))
def create_item(self, name: str) -> Item:
# If item name has a "!PROG" suffix, force it to be progression. This is typically used to create the right
# amount of progression rupees while keeping them a filler item as default
if name.endswith("!PROG"):
name = name.removesuffix("!PROG")
classification = ItemClassification.progression_deprioritized_skip_balancing
elif name.endswith("!USEFUL"):
# Same for above but with useful. This is typically used for Required Rings,
# as we don't want those locked in a barren dungeon
name = name.removesuffix("!USEFUL")
classification = ITEMS_DATA[name]["classification"]
if classification == ItemClassification.filler:
classification = ItemClassification.useful
elif name.endswith("!FILLER"):
name = name.removesuffix("!FILLER")
classification = ItemClassification.filler
else:
classification = ITEMS_DATA[name]["classification"]
ap_code = self.item_name_to_id[name]
# A few items become progression only in hard logic
progression_items_in_medium_logic = ["Expert's Ring", "Fist Ring", "Swimmer's Ring", "Energy Ring", "Heart Ring L-2"]
if self.options.logic_difficulty >= OracleOfSeasonsLogicDifficulty.option_medium and name in progression_items_in_medium_logic:
classification = ItemClassification.progression
if self.options.logic_difficulty >= OracleOfSeasonsLogicDifficulty.option_hard and name == "Heart Ring L-1":
classification = ItemClassification.progression
# As many Gasha Seeds become progression as the number of deterministic Gasha Nuts
if self.remaining_progressive_gasha_seeds > 0 and name == "Gasha Seed":
self.remaining_progressive_gasha_seeds -= 1
classification = ItemClassification.progression_deprioritized
# Players in Medium+ are expected to know the default paths through Lost Woods, Phonograph becomes filler
if self.options.logic_difficulty >= OracleOfSeasonsLogicDifficulty.option_medium and not self.options.randomize_lost_woods_item_sequence and name == "Phonograph":
classification = ItemClassification.filler
# UT doesn't let us know if the item is progression or not, so it is always progression
if hasattr(self.multiworld, "generation_is_fake"):
classification = ItemClassification.progression
return Item(name, classification, ap_code, self.player)
def create_items(self) -> None:
from .generation.CreateItems import create_items
create_items(self)
def get_pre_fill_items(self) -> list[Item]:
return self.pre_fill_items
@classmethod
def stage_pre_fill(cls, multiworld: MultiWorld):
from .generation.PreFill import stage_pre_fill_dungeon_items
stage_pre_fill_dungeon_items(multiworld)
def get_filler_item_name(self) -> str:
FILLER_ITEM_NAMES = [
"Rupees (1)", "Rupees (5)", "Rupees (10)", "Rupees (10)",
"Rupees (20)", "Rupees (30)",
"Ore Chunks (10)", "Ore Chunks (10)", "Ore Chunks (25)",
"Random Ring", "Random Ring", "Random Ring",
"Gasha Seed", "Gasha Seed",
"Potion"
]
item_name = self.random.choice(FILLER_ITEM_NAMES)
if item_name == "Random Ring":
return self.get_random_ring_name()
return item_name
def get_random_ring_name(self) -> str:
if len(self.random_rings_pool) > 0:
return self.random_rings_pool.pop()
return self.get_filler_item_name() # It might loop but not enough to really matter
def connect_entrances(self) -> None:
from .generation.ER import oos_randomize_entrances
oos_randomize_entrances(self)
# noinspection PyUnusedLocal
@classmethod
def stage_fill_hook(cls, multiworld: MultiWorld, progitempool: list[Item], usefulitempool: list[Item], filleritempool: list[Item], fill_locations):
from .generation.OrderPool import order_pool
order_pool(multiworld, progitempool)
def pre_output(self) -> None:
if self.options.bird_hint.know_it_all():
self.region_hints = create_region_hints(self)
if self.options.bird_hint.owl():
self.item_hints = create_item_hints(self)
def generate_output(self, output_directory: str) -> None:
from .generation.PatchWriter import oos_create_ap_procedure_patch
patch = oos_create_ap_procedure_patch(self)
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
f"{patch.patch_file_ending}")
patch.write(rom_path)
def fill_slot_data(self) -> dict:
slot_data = {
"version": f"{self.version()}",
"options": self.options.as_dict(
*[option_name for option_name in OracleOfSeasonsOptions.type_hints
if hasattr(OracleOfSeasonsOptions.type_hints[option_name], "include_in_slot_data")]),
# "samasa_gate_sequence": ' '.join([str(x) for x in self.samasa_gate_code]),
"lost_woods_item_sequence": self.lost_woods_item_sequence,
"lost_woods_main_sequence": self.lost_woods_main_sequence,
"default_seasons": self.default_seasons,
"old_man_rupee_values": self.old_man_rupee_values,
"dungeon_entrances": {a.replace(" entrance", ""): b.replace("enter ", "")
for a, b in self.dungeon_entrances.items()},
"essences_in_game": self.essences_in_game,
"subrosia_portals": self.portal_connections,
"shop_rupee_requirements": self.shop_rupee_requirements,
"shop_costs": self.shop_prices,
}
# The structure is made to make it easy to call CreateHints
slot_data_item_hints = []
for item_hint in self.item_hints:
if item_hint is None:
# Joke hint
slot_data_item_hints.append(None)
continue
location = item_hint.location
slot_data_item_hints.append((location.address, location.player))
slot_data["item_hints"] = slot_data_item_hints
return slot_data
def write_spoiler(self, spoiler_handle: TextIO):
from .generation.CreateRegions import location_is_active
spoiler_handle.write(f"\n\nDefault Seasons ({self.multiworld.player_name[self.player]}):\n")
for region_name, season in self.default_seasons.items():
spoiler_handle.write(f"\t- {region_name} --> {SEASON_NAMES[season]}\n")
if self.options.shuffle_dungeons:
spoiler_handle.write(f"\nDungeon Entrances ({self.multiworld.player_name[self.player]}):\n")
for entrance, dungeon in self.dungeon_entrances.items():
spoiler_handle.write(f"\t- {entrance} --> {dungeon.replace('enter ', '')}\n")
if self.options.shuffle_portals != "vanilla":
spoiler_handle.write(f"\nSubrosia Portals ({self.multiworld.player_name[self.player]}):\n")
for portal_holo, portal_sub in self.portal_connections.items():
spoiler_handle.write(f"\t- {portal_holo} --> {portal_sub}\n")
spoiler_handle.write(f"\nShop Prices ({self.multiworld.player_name[self.player]}):\n")
shop_codes = [code for shop in self.shop_order for code in shop]
shop_codes.extend(MARKET_LOCATIONS)
for shop_code in shop_codes:
price = self.shop_prices[shop_code]
for loc_name, loc_data in LOCATIONS_DATA.items():
if loc_data.get("symbolic_name", None) is None or loc_data["symbolic_name"] != shop_code:
continue
if location_is_active(self, loc_name, loc_data):
currency = "Ore Chunks" if shop_code.startswith("subrosia") else "Rupees"
spoiler_handle.write(f"\t- {loc_name}: {price} {currency}\n")
break
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if not change:
return False
mapping = self.item_mapping_collect.get(item.name, None)
if mapping is not None:
state.prog_items[self.player][mapping[0]] += mapping[1]
return True
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if not change:
return False
mapping = self.item_mapping_collect.get(item.name, None)
if mapping is not None:
state.prog_items[self.player][mapping[0]] -= mapping[1]
return True
# UT stuff
def interpret_slot_data(self, slot_data: Optional[dict[str, Any]]) -> Any:
if slot_data is not None:
return slot_data
if not hasattr(self.multiworld, "re_gen_passthrough") or self.game not in self.multiworld.re_gen_passthrough:
return False
slot_data = self.multiworld.re_gen_passthrough[self.game]
for option in [option_name for option_name in OracleOfSeasonsOptions.type_hints
if hasattr(OracleOfSeasonsOptions.type_hints[option_name], "include_in_slot_data")]:
option_class: Type[Option] = OracleOfSeasonsOptions.type_hints[option]
self.options.__setattr__(option, option_class.from_any(slot_data["options"][option]))
self.lost_woods_item_sequence = slot_data["lost_woods_item_sequence"]
self.lost_woods_main_sequence = slot_data["lost_woods_main_sequence"]
self.default_seasons = slot_data["default_seasons"]
self.old_man_rupee_values = slot_data["old_man_rupee_values"]
self.dungeon_entrances = {f"{a} entrance": f"enter {b}"
for a, b in slot_data["dungeon_entrances"].items()}
self.portal_connections = slot_data["subrosia_portals"]
self.shop_rupee_requirements = slot_data["shop_rupee_requirements"]
self.shop_prices = slot_data["shop_costs"]
return True