Files
dockipelago/worlds/sonic_heroes/__init__.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

984 lines
43 KiB
Python

from typing import TextIO, Callable, Any
from BaseClasses import *
from worlds.AutoWorld import World, WebWorld
from . import csvdata
from .constants import *
from .csvdata import * # type: ignore
from .items import * # type: ignore
from .logic_mapping_sonic import *
from .options import * # type: ignore
from .regions import *
class SonicHeroesWeb(WebWorld):
theme = PARTYTIMETHEME
setup_en = (Tutorial(
tutorial_name=TUTORIALNAME,
description=TUTORIALDESC,
language=TUTORIALLANGUAGE,
file_name=TUTORIALFILENAME,
link=TUTORIALLINK,
authors=TUTORIALAUTHORS
))
tutorials = [setup_en]
#option_groups = sonic_heroes_option_groups
game_info_languages = ["en"]
option_groups = sonic_heroes_option_groups
class SonicHeroesWorld(World):
game: ClassVar[str] = SONICHEROES
web: ClassVar[WebWorld] = SonicHeroesWeb()
options_dataclass = SonicHeroesOptions
options: SonicHeroesOptions
item_name_to_id: ClassVar[dict[str, int]] = \
{item.name: item.code for item in itemList}
location_name_to_id: ClassVar[dict[str, int]] = {loc.name: loc.code for loc in get_full_location_list()}
#{k: v for k, v in full_location_dict.items()}
item_name_groups: ClassVar[dict[str, set[str]]] = item_groups
location_name_groups: ClassVar[dict[str, set[str]]] = location_groups
topology_present: bool = True
#UT Stuff Here
ut_can_gen_without_yaml: ClassVar[bool] = True
id_offset: ClassVar[int] = 0x100
apworldversion: ClassVar[str] = "2.2.0"
@staticmethod
def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
return slot_data
def __init__(self, multiworld: MultiWorld, player: int):
#PUT STUFF HERE
#self.loc_id_to_loc = {}
self.secret = False
self.level_goal_event_locations: list[str] = []
self.team_level_goal_event_locations: dict[str, list[str]] = {}
self.bonus_key_event_items_per_team: dict[str, dict[str, list[str]]] = {}
self.region_to_location: dict[str, list[LocationCSVData]] = {}
self.region_list: list[RegionCSVData] = []
self.connection_list: list[ConnectionCSVData] = []
#self.logic_mapping_dict: dict[str, dict[str, dict[str, CollectionState]]] = {}
self.full_logic_mapping_dict: RejectDictionaryReturnToMonke[str, CollectionState] = RejectDictionaryReturnToMonke() # type: ignore
self.spoiler_string: str = ""
self.extra_items: int = 0
self.regular_regions: list[str] = \
[
OCEANREGION,
HOTPLANTREGION,
CASINOREGION,
TRAINREGION,
BIGPLANTREGION,
GHOSTREGION,
SKYREGION,
]
self.enabled_teams: list[str] = \
[
#SONIC,
#DARK,
#ROSE,
#CHAOTIX,
#SUPERHARD,
]
self.regular_levels: list[str] = \
[
SEASIDEHILL,
OCEANPALACE,
GRANDMETROPOLIS,
POWERPLANT,
CASINOPARK,
BINGOHIGHWAY,
RAILCANYON,
BULLETSTATION,
FROGFOREST,
LOSTJUNGLE,
HANGCASTLE,
MYSTICMANSION,
EGGFLEET,
FINALFORTRESS,
]
self.emerald_locations_added: list[str] = []
self.boss_locations_added: list[str] = []#\
#self.gate_num_to_boss_region:
self.regular_boss_levels = \
[
EGGHAWK,
TEAMFIGHT1,
ROBOTCARNIVAL,
EGGALBATROSS,
TEAMFIGHT2,
ROBOTSTORM,
EGGEMPEROR,
]
#self.allowed_levels: list[str] = []
self.allowed_levels_per_team: dict[str, list[str]] = {}
#self.should_make_puml: bool = True
self.should_make_puml_earlier: bool = False
self.highlight_unreachable_regions: bool = True
self.is_ut_gen: bool = False
self.legacy_gates_mode: bool = False
self.bonus_keys_needed_for_bonus_stage: int = 1
#self.goal_unlock_conditions: set[str] = set()
self.emblems_to_create: int = 0
self.required_emblems_ratio: float = 0.8
self.level_block_emblem_count: int = 14
self.gate_emblem_costs: list[int] = []
self.shuffled_levels: list[str] = []
self.shuffled_bosses: list[str] = []
self.gate_level_counts: list[int] = []
super().__init__(multiworld, player)
def create_item(self, name: str) -> "Item":
tempitems = [x for x in itemList if x.name == name]
if len(tempitems) == 0:
return SonicHeroesItem(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
return SonicHeroesItem(name, tempitems[0].classification, self.item_name_to_id[name], self.player)
def get_filler_item_name(self) -> str:
"""
Called when the item pool needs to be filled with additional items to match location count.
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
For most worlds this will be one or more of your filler items, but the classification of these items
does not need to be ItemClassification.filler.
The item name returned can be for a trap, useful, and/or progression item as long as it's repeatable.
"""
return self.random.choices(list(filler_items_to_weights.keys()), weights=list(filler_items_to_weights.values()), k=1)[0]
#return self.random.choice([item_data.name for item_data in itemList if item_data.classification == ItemClassification.filler and item_data.fillerweight > 0])
def generate_early(self) -> None:
#UT Stuff Here
self.handle_ut_yamlless(None)
# Check invalid options here
check_invalid_options(self)
#change stuff based on options
self.handle_option_checking()
#if self.settings.allow_debug_for_mod:
#self.force_super_hard_mode()
if self.options.unlock_type == UnlockType.option_legacy_level_gates:
self.handle_level_gates_start()
"""
#UT Stuff Here
if hasattr(self.multiworld, "re_gen_passthrough"):
if SONICHEROES not in self.multiworld.re_gen_passthrough:
return
passthrough = self.multiworld.re_gen_passthrough[SONICHEROES]
self.options.goal_unlock_condition = passthrough["goal_unlock_condition"]
"""
create_special_region_csv_data(self)
if SONIC in self.enabled_teams:
self.init_logic_mapping_sonic()
#dont need other teams as only rules are empty string
for team in self.enabled_teams:
#import csv data
self.import_csv_data(team)
# level completion event locs
self.team_level_goal_event_locations[team] = []
self.bonus_key_event_items_per_team[team] = {}
for level in self.allowed_levels_per_team[team]:
self.bonus_key_event_items_per_team[team][level] = []
#map regions
#map_sonic_regions(self)
#map locations
#map_sonic_locations(self)
#map connections
#map_sonic_connections(self)
pass
def create_regions(self) -> None:
create_regions(self)
victory_item = SonicHeroesItem(VICTORYITEM, ItemClassification.progression, None, self.player)
self.get_location(VICTORYLOCATION).place_locked_item(victory_item)
#print(self.level_goal_event_locations)
index = 1 if self.secret else 0
for team in self.allowed_levels_per_team.keys():
for loc_name in self.team_level_goal_event_locations[team]:
#global level completion
goal_unlock_item = SonicHeroesItem(f"{LEVEL} {COMPLETIONEVENT}", ItemClassification.progression, None, self.player)
self.get_location(f"{loc_name} {team} Goal Event Location").place_locked_item(goal_unlock_item)
#level completion for team
goal_unlock_item = SonicHeroesItem(f"{team} {loc_name} {COMPLETIONEVENT}", ItemClassification.progression, None, self.player)
self.get_location(f"{loc_name} {team} Goal Event Location For Team").place_locked_item(goal_unlock_item)
for level in self.allowed_levels_per_team[team]:
for key in range(bonus_key_amounts[team][level][index]):
self.bonus_key_event_items_per_team[team][level].append(f"{team} {level} Bonus Key #{key + 1} Event")
key_event_item = SonicHeroesItem(f"{team} {level} Bonus Key #{key + 1} Event", ItemClassification.progression, None, self.player)
self.get_location(f"{level} {team} Bonus Key {key + 1} Event").place_locked_item(key_event_item)
#boss events
if self.options.unlock_type == UnlockType.option_legacy_level_gates:
for gate_num, boss in enumerate(self.boss_locations_added):
boss_event_item = SonicHeroesItem(f"Gate {gate_num} Boss: {boss}", ItemClassification.progression, None, self.player)
#print(f"Looking for Event Location: {boss} Event Location")
self.get_location(f"{boss} Event Location").place_locked_item(boss_event_item)
pass
def create_items(self) -> None:
create_items(self)
if self.options.unlock_type == UnlockType.option_ability_character_unlocks:
self.multiworld.push_precollected(self.create_item(f"{SONIC} {JUMP}"))
self.multiworld.push_precollected(self.create_item(f"{SONIC} {POWERATTACK}"))
if self.options.sonic_story_starting_character == SonicStoryStartingCharacter.option_sonic:
self.multiworld.push_precollected(self.create_item(PLAYABLESONIC))
elif self.options.sonic_story_starting_character == SonicStoryStartingCharacter.option_tails:
self.multiworld.push_precollected(self.create_item(PLAYABLETAILS))
elif self.options.sonic_story_starting_character == SonicStoryStartingCharacter.option_knuckles:
self.multiworld.push_precollected(self.create_item(PLAYABLEKNUCKLES))
else:
print("Cannot Determine Starting Character. Please Help")
pass
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: state.has(VICTORYITEM, self.player)
pass
def connect_entrances(self) -> None:
connect_entrances(self)
pass
def generate_basic(self) -> None:
pass
def pre_fill(self) -> None:
#self.make_puml()
pass
def fill_hook(self, progitempool: list[Item], usefulitempool: list[Item], filleritempool: list[Item], fill_locations: list[Location]) -> None:
pass
def post_fill(self) -> None:
if self.should_make_puml_earlier:
self.make_puml()
pass
def generate_output(self, output_directory: str) -> None:
pass
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
#Location: "Hint"
pass
def fill_slot_data(self) -> Mapping[str, Any]:
if self.options.make_puml:
self.make_puml()
if self.options.unlock_type == UnlockType.option_ability_character_unlocks:
self.gate_emblem_costs = [0]
self.shuffled_levels = [f"S{x}" for x in range(2, 16)]
self.shuffled_bosses = ["B23"]
self.gate_level_counts = [14]
return \
{
"APWorldVersion": self.apworldversion,
"IncludedLevelsAndSanities": self.options.included_levels_and_sanities.value,
"UnlockType": self.options.unlock_type.value,
"AbilityUnlocks": self.options.ability_unlocks.value,
"LegacyNumberOfLevelGates": self.options.legacy_number_of_level_gates.value,
"LegacyLevelGatesAllowedBosses": self.options.legacy_level_gates_allowed_bosses.value,
"RequiredRank": self.options.required_rank.value,
"FinalBoss": self.options.final_boss.value,
"GoalUnlockConditions": self.options.goal_unlock_conditions.value,
"GoalLevelCompletions": self.options.goal_level_completions.value,
"GoalLevelCompletionsPerStory": self.options.goal_level_completions_per_story.value,
#"SonicStory": self.options.sonic_story.value,
"SonicStoryStartingCharacter": self.options.sonic_story_starting_character.value,
#"SonicKeySanity": self.options.sonic_key_sanity.value,
#"SonicCheckpointSanity": self.options.sonic_checkpoint_sanity.value,
#"DarkStory": self.options.dark_story.value,
"DarkSanity": self.options.dark_sanity.value,
"DarkStoryStartingCharacter": self.options.dark_story_starting_character.value,
#"DarkKeySanity": self.options.dark_key_sanity.value,
#"DarkCheckpointSanity": self.options.dark_checkpoint_sanity.value,
#"RoseStory": self.options.rose_story.value,
"RoseSanity": self.options.rose_sanity.value,
"RoseStoryStartingCharacter": self.options.rose_story_starting_character.value,
#"RoseKeySanity": self.options.rose_key_sanity.value,
#"RoseCheckpointSanity": self.options.rose_checkpoint_sanity.value,
#"ChaotixStory": self.options.chaotix_story.value,
"ChaotixSanity": self.options.chaotix_sanity.value,
"ChaotixStoryStartingCharacter": self.options.chaotix_story_starting_character.value,
#"ChaotixKeySanity": self.options.chaotix_key_sanity.value,
#"ChaotixCheckpointSanity": self.options.chaotix_checkpoint_sanity.value,
#"SuperHardMode": self.options.super_hard_mode.value,
"SuperHardModeStartingCharacter": self.options.super_hard_mode_starting_character.value,
#"SuperHardModeCheckpointSanity": self.options.super_hard_mode_checkpoint_sanity.value,
#"RingLink": self.options.ring_link.value,
#"RingLinkOverlord": self.options.ring_link_metal_overlord.value,
#"DeathLink": 0,
#"ModernRingLoss": self.options.modern_ring_loss.value,
"RemoveCasinoParkVIPTableLaserGate": self.options.remove_casino_park_vip_table_laser_gate.value,
"GateEmblemCosts": self.gate_emblem_costs,
"ShuffledLevels": self.shuffled_levels,
"ShuffledBosses": self.shuffled_bosses,
"GateLevelCounts": self.gate_level_counts,
}
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
spoiler_handle.write(self.spoiler_string)
#print(self.item_name_groups)
#print(self.location_name_groups)
pass
def write_spoiler(self, spoiler_handle: TextIO) -> None:
pass
def write_spoiler_end(self, spoiler_handle: TextIO) -> None:
pass
def make_puml(self):
if self.player_name[0:1].isdigit():
return
from Utils import visualize_regions
state = self.multiworld.get_all_state()
state.update_reachable_regions(self.player)
reachable_regions = state.reachable_regions[self.player]
unreachable_regions: set[Region] = set() # type: ignore
for region in self.multiworld.regions:
if region not in reachable_regions:
unreachable_regions.add(region)
if self.highlight_unreachable_regions:
visualize_regions(self.get_region("Menu"), f"{self.player_name}_world.puml", show_entrance_names=True, regions_to_highlight=unreachable_regions)
else:
visualize_regions(self.get_region("Menu"), f"{self.player_name}_world.puml", show_entrance_names=True, regions_to_highlight=reachable_regions)
# !pragma layout smetana
# put this at top to display PUML (after start UML)
def import_csv_data(self, team: str):
#Regions First
import_region_csv(self, team)
#Locations Next
import_location_csv(self, team)
#Connections Third
import_connection_csv(self, team)
def init_logic_mapping_sonic(self) -> None:
self.full_logic_mapping_dict.update(create_logic_mapping_dict_seaside_hill_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_ocean_palace_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_grand_metropolis_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_power_plant_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_casino_park_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_bingo_highway_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_rail_canyon_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_bullet_station_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_frog_forest_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_lost_jungle_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_hang_castle_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_mystic_mansion_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_egg_fleet_sonic(self))
self.full_logic_mapping_dict.update(create_logic_mapping_dict_final_fortress_sonic(self))
"""
return \
{
SEASIDEHILL: create_logic_mapping_dict_seaside_hill_sonic(self),
OCEANPALACE: create_logic_mapping_dict_ocean_palace_sonic(self),
GRANDMETROPOLIS: create_logic_mapping_dict_grand_metropolis_sonic(self),
POWERPLANT: create_logic_mapping_dict_power_plant_sonic(self),
CASINOPARK: create_logic_mapping_dict_casino_park_sonic(self),
BINGOHIGHWAY: create_logic_mapping_dict_bingo_highway_sonic(self),
RAILCANYON: create_logic_mapping_dict_rail_canyon_sonic(self),
BULLETSTATION: create_logic_mapping_dict_bullet_station_sonic(self),
FROGFOREST: create_logic_mapping_dict_frog_forest_sonic(self),
LOSTJUNGLE: create_logic_mapping_dict_lost_jungle_sonic(self),
HANGCASTLE: create_logic_mapping_dict_hang_castle_sonic(self),
MYSTICMANSION: create_logic_mapping_dict_mystic_mansion_sonic(self),
EGGFLEET: create_logic_mapping_dict_egg_fleet_sonic(self),
FINALFORTRESS: create_logic_mapping_dict_final_fortress_sonic(self),
}
"""
def init_logic_mapping_any_team(self) -> dict[str, dict[str, CollectionState]]:
# noinspection PyTypeChecker
rule_dict: dict[str, dict[str, CollectionState]] = \
{
METALMADNESS:
{
"": lambda state: True, # type: ignore
},
}
rule_dict.update({name: {"": lambda state: True} for name in bonus_and_emerald_stages}) # type: ignore
return rule_dict
# noinspection PyTypeChecker
def init_full_logic_mapping_defaults(self) -> None:
rule_dict: dict[str, CollectionState] = \
{
"": lambda state: True, # type: ignore
"NOTPOSSIBLE": lambda state: False, # type: ignore
}
self.full_logic_mapping_dict.update(rule_dict)
def handle_ut_yamlless(self, slot_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
if not slot_data \
and hasattr(self.multiworld, "re_gen_passthrough") \
and isinstance(self.multiworld.re_gen_passthrough, dict) \
and self.game in self.multiworld.re_gen_passthrough:
slot_data = self.multiworld.re_gen_passthrough[self.game]
if not slot_data:
return None
print(f"USING UT RE GEN PASSTHROUGH HERE")
self.is_ut_gen = True
self.options.included_levels_and_sanities.value = slot_data["IncludedLevelsAndSanities"]
self.options.unlock_type.value = slot_data["UnlockType"]
self.options.ability_unlocks.value = slot_data["AbilityUnlocks"]
self.options.legacy_number_of_level_gates.value = slot_data["LegacyNumberOfLevelGates"]
self.options.legacy_level_gates_allowed_bosses.value = slot_data["LegacyLevelGatesAllowedBosses"]
self.options.required_rank.value = slot_data["RequiredRank"]
self.options.final_boss.value = slot_data["FinalBoss"]
self.options.goal_unlock_conditions.value = slot_data["GoalUnlockConditions"]
self.options.goal_level_completions.value = slot_data["GoalLevelCompletions"]
self.options.goal_level_completions_per_story.value = slot_data["GoalLevelCompletionsPerStory"]
#self.options.sonic_story.value = slot_data["SonicStory"]
self.options.sonic_story_starting_character.value = slot_data["SonicStoryStartingCharacter"]
#self.options.sonic_key_sanity.value = slot_data["SonicKeySanity"]
#self.options.sonic_checkpoint_sanity.value = slot_data["SonicCheckpointSanity"]
#self.options.dark_story.value = slot_data["DarkStory"]
self.options.dark_sanity.value = slot_data["DarkSanity"]
self.options.dark_story_starting_character.value = slot_data["DarkStoryStartingCharacter"]
#self.options.dark_key_sanity.value = slot_data["DarkKeySanity"]
#self.options.dark_checkpoint_sanity.value = slot_data["DarkCheckpointSanity"]
#self.options.rose_story.value = slot_data["RoseStory"]
self.options.rose_sanity.value = slot_data["RoseSanity"]
self.options.rose_story_starting_character.value = slot_data["RoseStoryStartingCharacter"]
#self.options.rose_key_sanity.value = slot_data["RoseKeySanity"]
#self.options.rose_checkpoint_sanity.value = slot_data["RoseCheckpointSanity"]
#self.options.chaotix_story.value = slot_data["ChaotixStory"]
self.options.chaotix_sanity.value = slot_data["ChaotixSanity"]
self.options.chaotix_story_starting_character.value = slot_data["ChaotixStoryStartingCharacter"]
#self.options.chaotix_key_sanity.value = slot_data["ChaotixKeySanity"]
#self.options.chaotix_checkpoint_sanity.value = slot_data["ChaotixCheckpointSanity"]
#self.options.super_hard_mode.value = slot_data["SuperHardMode"]
self.options.super_hard_mode_starting_character.value = slot_data["SuperHardModeStartingCharacter"]
#self.options.super_hard_mode_checkpoint_sanity.value = slot_data["SuperHardModeCheckpointSanity"]
#self.options.ring_link.value = slot_data["RingLink"]
#self.options.ring_link_metal_overlord.value = slot_data["RingLinkMetalOverlord"]
#self.options.death_link.value = slot_data["DeathLink"]
#self.options.modern_ring_loss.value = slot_data["ModernRingLoss"]
self.options.remove_casino_park_vip_table_laser_gate.value = slot_data["RemoveCasinoParkVIPTableLaserGate"]
self.gate_emblem_costs = slot_data["GateEmblemCosts"]
self.shuffled_levels = slot_data["ShuffledLevels"]
self.shuffled_bosses = slot_data["ShuffledBosses"]
self.gate_level_counts = slot_data["GateLevelCounts"]
return slot_data
def handle_option_checking(self) -> None:
if not LEVELCOMPLETIONSALLTEAMS in self.options.goal_unlock_conditions:
self.options.goal_level_completions.value = 0
if not LEVELCOMPLETIONSPERSTORY in self.options.goal_unlock_conditions:
self.options.goal_level_completions_per_story.value = 0
if self.is_this_team_enabled(SONIC):
self.enabled_teams.append(SONIC)
self.allowed_levels_per_team[SONIC] = self.regular_levels
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(SONIC, both_acts_required=True):
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(DARK):
self.enabled_teams.append(DARK)
self.allowed_levels_per_team[DARK] = self.regular_levels
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(DARK, both_acts_required=True):
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(ROSE):
self.enabled_teams.append(ROSE)
self.allowed_levels_per_team[ROSE] = self.regular_levels
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(ROSE, both_acts_required=True):
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(CHAOTIX):
self.enabled_teams.append(CHAOTIX)
self.allowed_levels_per_team[CHAOTIX] = self.regular_levels
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(CHAOTIX, both_acts_required=True):
self.emblems_to_create += self.level_block_emblem_count
if self.is_this_team_enabled(SUPERHARDMODE):
self.enabled_teams.append(SUPERHARDMODE)
self.allowed_levels_per_team[SUPERHARDMODE] = self.regular_levels
self.emblems_to_create += self.level_block_emblem_count
if self.options.unlock_type == UnlockType.option_legacy_level_gates:
if self.is_this_sanity_enabled(ROSE, OBJSANITY) or self.is_this_sanity_enabled(CHAOTIX, OBJSANITY):
change_filler_weights_for_legacy_level_gates(self)
def handle_level_gates_start(self) -> None:
if not self.is_ut_gen:
self.generate_level_gates()
for gate_num in range(self.options.legacy_number_of_level_gates.value):
self.boss_locations_added.append(sonic_heroes_extra_names[int(self.shuffled_bosses[gate_num][1:]) - 16])
self.region_to_location[sonic_heroes_extra_names[int(self.shuffled_bosses[gate_num][1:]) - 16]] = []
def generate_level_gates(self) -> None:
shuffleable_levels: list[str] = []
team_letter: str = 'S'
"""S D R C based on team (superhard is part of sonic)"""
act_letter: str = 'A'
"""A B or C based on acts enabled"""
if self.is_this_team_enabled(SONIC) or self.is_this_team_enabled(SUPERHARDMODE):
team_letter = 'S'
if SONICACTA in self.options.included_levels_and_sanities and not self.is_this_team_enabled(SONIC, both_acts_required=True) and not self.is_this_team_enabled(SUPERHARDMODE):
act_letter = 'A'
elif (SONICACTB in self.options.included_levels_and_sanities or self.is_this_team_enabled(SUPERHARDMODE)) and not SONICACTA in self.options.included_levels_and_sanities:
act_letter = 'B'
elif (SONICACTB in self.options.included_levels_and_sanities or self.is_this_team_enabled(SUPERHARDMODE)) and SONICACTA in self.options.included_levels_and_sanities:
act_letter = 'C'
if self.is_this_team_enabled(SONIC):
for level in self.allowed_levels_per_team[SONIC]:
shuffleable_levels.append(f"{team_letter}{sonic_heroes_regular_levels_index[level]}")
elif self.is_this_team_enabled(SUPERHARDMODE):
for level in self.allowed_levels_per_team[SUPERHARDMODE]:
shuffleable_levels.append(f"{team_letter}{sonic_heroes_regular_levels_index[level]}")
if self.is_this_team_enabled(DARK):
team_letter = 'D'
if DARKACTA in self.options.included_levels_and_sanities and not self.is_this_team_enabled(DARK, both_acts_required=True):
act_letter = 'A'
elif DARKACTB in self.options.included_levels_and_sanities and not self.is_this_team_enabled(DARK, both_acts_required=True):
act_letter = 'B'
elif self.is_this_team_enabled(DARK, both_acts_required=True):
act_letter = 'C'
for level in self.allowed_levels_per_team[DARK]:
shuffleable_levels.append(f"{team_letter}{sonic_heroes_regular_levels_index[level]}")
if self.is_this_team_enabled(ROSE):
team_letter = 'R'
if ROSEACTA in self.options.included_levels_and_sanities and not self.is_this_team_enabled(ROSE, both_acts_required=True):
act_letter = 'A'
elif ROSEACTB in self.options.included_levels_and_sanities and not self.is_this_team_enabled(ROSE, both_acts_required=True):
act_letter = 'B'
elif self.is_this_team_enabled(ROSE, both_acts_required=True):
act_letter = 'C'
for level in self.allowed_levels_per_team[ROSE]:
shuffleable_levels.append(f"{team_letter}{sonic_heroes_regular_levels_index[level]}")
if self.is_this_team_enabled(CHAOTIX):
team_letter = 'C'
if CHAOTIXACTA in self.options.included_levels_and_sanities and not self.is_this_team_enabled(CHAOTIX, both_acts_required=True):
act_letter = 'A'
elif CHAOTIXACTB in self.options.included_levels_and_sanities and not self.is_this_team_enabled(CHAOTIX, both_acts_required=True):
act_letter = 'B'
elif self.is_this_team_enabled(CHAOTIX, both_acts_required=True):
act_letter = 'C'
for level in self.allowed_levels_per_team[CHAOTIX]:
shuffleable_levels.append(f"{team_letter}{sonic_heroes_regular_levels_index[level]}")
#0-13 Sonic
#14-27 Dark
#28-41 Rose
#42-55 Chaotix
#Now Do Bosses
shuffleable_bosses: list[str] = list(self.options.legacy_level_gates_allowed_bosses.value.copy())
self.random.shuffle(shuffleable_levels)
self.random.shuffle(shuffleable_bosses)
for _ in range(len(shuffleable_bosses) - self.options.legacy_number_of_level_gates.value):
shuffleable_bosses.pop()
number_level_groups = self.options.legacy_number_of_level_gates.value + 1
minimum_levels_per_group = len(shuffleable_levels) // number_level_groups
remainder_levels = len(shuffleable_levels) % number_level_groups
final_boss_emblem_cost = int(self.emblems_to_create * self.required_emblems_ratio)
first_gate_cost = final_boss_emblem_cost // number_level_groups
for level_group_number in range(number_level_groups):
go_to_next_group: bool = False
has_used_remainder_level: bool = False
num_locations_in_group: int = 0
num_levels_in_group: int = 0
while not go_to_next_group:
current_level_entry: str = shuffleable_levels.pop(0)
current_team: str = team_code_to_team[current_level_entry[0]]
current_level: str = sonic_heroes_level_names[int(current_level_entry[1:]) + 1]
#num_locations_at_current_level: int = try_to_guess_how_many_locations_are_here(self, current_team, current_level)
num_locations_in_group += self.try_to_guess_how_many_locations_are_here(current_team, current_level)
if current_team == SONIC:
#num_locations_at_current_level += try_to_guess_how_many_locations_are_here(self, SUPERHARDMODE, current_level)
num_locations_in_group += self.try_to_guess_how_many_locations_are_here(SUPERHARDMODE, current_level)
#add level here
self.shuffled_levels.append(f"{current_level_entry[0]}{int(current_level_entry[1:]) + 2}")
num_levels_in_group += 1
if len(shuffleable_levels) == 0 or (num_locations_in_group >= first_gate_cost and num_levels_in_group >= minimum_levels_per_group):
go_to_next_group = True
if not has_used_remainder_level and remainder_levels > 0:
has_used_remainder_level = False
remainder_levels -= 1
go_to_next_group = False
self.gate_level_counts.append(num_levels_in_group)
if level_group_number < number_level_groups - 1:
self.gate_emblem_costs.append(first_gate_cost * (level_group_number + 1))
self.shuffled_bosses.append(f"B{boss_name_to_slot_data_id[shuffleable_bosses.pop(0)]}")
else:
self.gate_emblem_costs.append(final_boss_emblem_cost)
self.shuffled_bosses.append(f"B{boss_name_to_slot_data_id[METALMADNESS]}")
def force_super_hard_mode(self):
if self.options.ability_unlocks == AbilityUnlocks.option_all_regions_separate:
self.options.ability_unlocks.value = AbilityUnlocks.option_entire_story
if SONICACTB in self.options.included_levels_and_sanities:
self.options.included_levels_and_sanities.value.remove(SONICACTB)
if SONICKEYSANITYBOTHACTS in self.options.included_levels_and_sanities:
self.options.included_levels_and_sanities.value.remove(SONICKEYSANITYBOTHACTS)
self.options.included_levels_and_sanities.value.add(SONICKEYSANITY1SET)
if SONICCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities:
self.options.included_levels_and_sanities.value.remove(SONICCHECKPOINTSANITYBOTHACTS)
self.options.included_levels_and_sanities.value.add(SONICCHECKPOINTSANITY1SET)
self.options.included_levels_and_sanities.value.add(SUPERHARDMODE)
def try_to_guess_how_many_locations_are_here(self, team: str, level: str) -> int:
"""
This is really stupid, why do I need to do this again?
Future me says it to prevent fill errors, apparently.
"""
num_locations = 0
secret_index = 1 if self.secret else 0
if self.is_this_team_enabled(team):
num_locations += 1
if self.is_this_sanity_enabled(team, KEYSANITY):
num_locations += bonus_keys_per_team_level[team][level][secret_index]
if self.is_this_sanity_enabled(team, CHECKPOINTSANITY):
num_locations += checkpoints_per_team_level[team][level][secret_index]
if self.is_this_sanity_enabled(team, OBJSANITY):
num_locations += self.get_number_of_obj_sanity_checks(team, level)
if self.is_this_team_enabled(team, both_acts_required=True):
num_locations += 1
if self.is_this_sanity_enabled(team, KEYSANITY, both_acts_required=True):
num_locations += bonus_keys_per_team_level[team][level][secret_index]
if self.is_this_sanity_enabled(team, CHECKPOINTSANITY, both_acts_required=True):
num_locations += checkpoints_per_team_level[team][level][secret_index]
return num_locations
def get_number_of_obj_sanity_checks(self, team: str, level: str) -> int:
num_locations: int = 0
if team == DARK:
num_locations += int(100 / self.options.dark_sanity.value) if DARKACTB in self.options.included_levels_and_sanities else 0
elif team == ROSE:
num_locations += int(200 / self.options.rose_sanity.value) if ROSEACTB in self.options.included_levels_and_sanities else 0
elif team == CHAOTIX:
if self.is_this_team_enabled(CHAOTIX, both_acts_required=True):
if level == CASINOPARK:
num_locations += int(700 / self.options.chaotix_sanity.value)
else:
num_locations += chaotix_obj_sanity_checks_per_level_act[level][0] + chaotix_obj_sanity_checks_per_level_act[level][1]
elif CHAOTIXACTB in self.options.included_levels_and_sanities:
if level == CASINOPARK:
num_locations += int(500 / self.options.chaotix_sanity.value)
else:
num_locations += chaotix_obj_sanity_checks_per_level_act[level][1]
elif CHAOTIXACTA in self.options.included_levels_and_sanities:
if level == CASINOPARK:
num_locations += int(200 / self.options.chaotix_sanity.value)
else:
num_locations += chaotix_obj_sanity_checks_per_level_act[level][0]
return num_locations
def is_this_team_enabled(self, team: str, both_acts_required: bool = False) -> bool:
"""
Returns True if either Act of the team is enabled.
both_acts_required requires both acts to be enabled to return True (Super Hard Mode ignores this).
"""
if team == SONIC:
if both_acts_required:
return SONICACTA in self.options.included_levels_and_sanities and SONICACTB in self.options.included_levels_and_sanities
return SONICACTA in self.options.included_levels_and_sanities or SONICACTB in self.options.included_levels_and_sanities
if team == DARK:
if both_acts_required:
return DARKACTA in self.options.included_levels_and_sanities and DARKACTB in self.options.included_levels_and_sanities
return DARKACTA in self.options.included_levels_and_sanities or DARKACTB in self.options.included_levels_and_sanities
if team == ROSE:
if both_acts_required:
return ROSEACTA in self.options.included_levels_and_sanities and ROSEACTB in self.options.included_levels_and_sanities
return ROSEACTA in self.options.included_levels_and_sanities or ROSEACTB in self.options.included_levels_and_sanities
if team == CHAOTIX:
if both_acts_required:
return CHAOTIXACTA in self.options.included_levels_and_sanities and CHAOTIXACTB in self.options.included_levels_and_sanities
return CHAOTIXACTA in self.options.included_levels_and_sanities or CHAOTIXACTB in self.options.included_levels_and_sanities
if team == SUPERHARDMODE:
#if both_acts_required:
#print(f"Both Acts asked for in is_this_team_enabled for team Super Hard Mode.")
return SUPERHARDMODE in self.options.included_levels_and_sanities
if team == ANYTEAM:
return False
print(f"Wrong team {team} in is_this_team_enabled")
return False
def is_this_sanity_enabled(self, team: str, sanity: str, both_acts_required: bool = False) -> bool:
if team == SONIC:
if sanity == KEYSANITY:
if both_acts_required:
return SONICKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
return SONICKEYSANITY1SET in self.options.included_levels_and_sanities or SONICKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == CHECKPOINTSANITY:
if both_acts_required:
return SONICCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
return SONICCHECKPOINTSANITY1SET in self.options.included_levels_and_sanities or SONICCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == OBJSANITY:
return False
print(f"Team {team} does not have sanity {sanity} in is_this_sanity_enabled")
return False
if team == DARK:
if sanity == KEYSANITY:
if both_acts_required:
return DARKKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
return DARKKEYSANITY1SET in self.options.included_levels_and_sanities or DARKKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == CHECKPOINTSANITY:
if both_acts_required:
return DARKCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
return DARKCHECKPOINTSANITY1SET in self.options.included_levels_and_sanities or DARKCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == OBJSANITY:
return DARKOBJSANITY in self.options.included_levels_and_sanities
#return self.options.dark_sanity.value > 0
print(f"Team {team} does not have sanity {sanity} in is_this_sanity_enabled")
return False
if team == ROSE:
if sanity == KEYSANITY:
if both_acts_required:
return ROSEKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
return ROSEKEYSANITY1SET in self.options.included_levels_and_sanities or ROSEKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == CHECKPOINTSANITY:
if both_acts_required:
return ROSECHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
return ROSECHECKPOINTSANITY1SET in self.options.included_levels_and_sanities or ROSECHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == OBJSANITY:
return ROSEOBJSANITY in self.options.included_levels_and_sanities
print(f"Team {team} does not have sanity {sanity} in is_this_sanity_enabled")
return False
if team == CHAOTIX:
if sanity == KEYSANITY:
if both_acts_required:
return CHAOTIXKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
return CHAOTIXKEYSANITY1SET in self.options.included_levels_and_sanities or CHAOTIXKEYSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == CHECKPOINTSANITY:
if both_acts_required:
return CHAOTIXCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
return CHAOTIXCHECKPOINTSANITY1SET in self.options.included_levels_and_sanities or CHAOTIXCHECKPOINTSANITYBOTHACTS in self.options.included_levels_and_sanities
if sanity == OBJSANITY:
return CHAOTIXOBJSANITY in self.options.included_levels_and_sanities
print(f"Team {team} does not have sanity {sanity} in is_this_sanity_enabled")
return False
if team == SUPERHARDMODE:
if sanity == KEYSANITY:
return False
if sanity == CHECKPOINTSANITY:
if both_acts_required:
#print(f"Both Acts asked for in is_this_sanity_enabled for team Super Hard Mode.")
return False
return SUPERHARDCHECKPOINTSANITY in self.options.included_levels_and_sanities
if sanity == OBJSANITY:
return False
print(f"Team {team} does not have sanity {sanity} in is_this_sanity_enabled")
return False
if team == ANYTEAM:
return False
print(f"How did we get here? Team {team} in is_this_sanity_enabled")
return False