Timespinner: Refactorings + fix for #1460 (#1484)

This commit is contained in:
Jarno
2023-03-04 08:16:05 +01:00
committed by GitHub
parent 9fa1f4e85f
commit a4b61118cf
3 changed files with 305 additions and 339 deletions

View File

@@ -1,10 +1,10 @@
from typing import Dict, List, Set, Tuple, TextIO
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from typing import Dict, List, Set, Tuple, TextIO, Union
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, filler_items
from .Locations import get_locations, EventId
from .Locations import get_location_datas, EventId
from .Options import is_option_enabled, get_option_value, timespinner_options
from .PreCalculatedWeights import PreCalculatedWeights
from .Regions import create_regions
from .Regions import create_regions_and_locations
from worlds.AutoWorld import World, WebWorld
class TimespinnerWebWorld(WebWorld):
@@ -29,7 +29,6 @@ class TimespinnerWebWorld(WebWorld):
tutorials = [setup, setup_de]
class TimespinnerWorld(World):
"""
Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers.
@@ -44,21 +43,16 @@ class TimespinnerWorld(World):
required_client_version = (0, 3, 7)
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None, None)}
location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)}
item_name_groups = get_item_names_per_category()
locked_locations: List[str]
location_cache: List[Location]
precalculated_weights: PreCalculatedWeights
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.locked_locations = []
self.location_cache = []
self.precalculated_weights = PreCalculatedWeights(world, player)
def generate_early(self):
def generate_early(self) -> None:
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0:
self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
@@ -67,44 +61,27 @@ class TimespinnerWorld(World):
if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0:
self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true
def create_regions(self):
locations = get_locations(self.multiworld, self.player, self.precalculated_weights)
create_regions(self.multiworld, self.player, locations, self.location_cache, self.precalculated_weights)
def create_regions(self) -> None:
create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights)
def create_item(self, name: str) -> Item:
return create_item_with_correct_settings(self.multiworld, self.player, name)
def create_items(self) -> None:
self.create_and_assign_event_items()
def get_filler_item_name(self) -> str:
trap_chance: int = get_option_value(self.multiworld, self.player, "TrapChance")
enabled_traps: List[str] = get_option_value(self.multiworld, self.player, "Traps")
excluded_items: Set[str] = self.get_excluded_items()
if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps:
return self.multiworld.random.choice(enabled_traps)
else:
return self.multiworld.random.choice(filler_items)
self.assign_starter_items(excluded_items)
def set_rules(self):
setup_events(self.player, self.locked_locations, self.location_cache)
self.multiworld.itempool += self.get_item_pool(excluded_items)
def set_rules(self) -> None:
final_boss: str
if is_option_enabled(self.multiworld, self.player, "DadPercent"):
if self.is_option_enabled("DadPercent"):
final_boss = "Killed Emperor"
else:
final_boss = "Killed Nightmare"
self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player)
def generate_basic(self):
excluded_items: Set[str] = get_excluded_items(self, self.multiworld, self.player)
assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations)
pool = get_item_pool(self.multiworld, self.player, excluded_items)
fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool)
self.multiworld.itempool += pool
def fill_slot_data(self) -> Dict[str, object]:
slot_data: Dict[str, object] = {}
@@ -112,12 +89,12 @@ class TimespinnerWorld(World):
for option_name in timespinner_options:
if (option_name not in ap_specific_settings):
slot_data[option_name] = get_option_value(self.multiworld, self.player, option_name)
slot_data[option_name] = self.get_option_value(option_name)
slot_data["StinkyMaw"] = True
slot_data["ProgressiveVerticalMovement"] = False
slot_data["ProgressiveKeycards"] = False
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache)
slot_data["PersonalItems"] = self.get_personal_items()
slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock
slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock
slot_data["PastGate"] = self.precalculated_weights.past_key_unlock
@@ -135,17 +112,17 @@ class TimespinnerWorld(World):
return slot_data
def write_spoiler_header(self, spoiler_handle: TextIO):
if is_option_enabled(self.multiworld, self.player, "UnchainedKeys"):
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.is_option_enabled("UnchainedKeys"):
spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n')
spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n')
if is_option_enabled(self.multiworld, self.player, "EnterSandman"):
if self.is_option_enabled("EnterSandman"):
spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n')
else:
spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n')
if is_option_enabled(self.multiworld, self.player, "RisingTides"):
if self.is_option_enabled("RisingTides"):
flooded_areas: List[str] = []
if self.precalculated_weights.flood_basement:
@@ -177,133 +154,131 @@ class TimespinnerWorld(World):
spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n')
def create_item(self, name: str) -> Item:
data = item_table[name]
def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]:
excluded_items: Set[str] = set()
if is_option_enabled(world, player, "StartWithJewelryBox"):
excluded_items.add('Jewelry Box')
if is_option_enabled(world, player, "StartWithMeyef"):
excluded_items.add('Meyef')
if is_option_enabled(world, player, "QuickSeed"):
excluded_items.add('Talaria Attachment')
if is_option_enabled(world, player, "UnchainedKeys"):
excluded_items.add('Twin Pyramid Key')
if not is_option_enabled(world, player, "EnterSandman"):
excluded_items.add('Mysterious Warp Beacon')
else:
excluded_items.add('Timeworn Warp Beacon')
excluded_items.add('Modern Warp Beacon')
excluded_items.add('Mysterious Warp Beacon')
for item in world.precollected_items[player]:
if item.name not in self.item_name_groups['UseItem']:
excluded_items.add(item.name)
return excluded_items
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
non_local_items = world.non_local_items[player].value
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items)
if not local_starter_melee_weapons:
if 'Plasma Orb' in non_local_items:
raise Exception("Atleast one melee orb must be local")
if data.useful:
classification = ItemClassification.useful
elif data.progression:
classification = ItemClassification.progression
elif data.trap:
classification = ItemClassification.trap
else:
local_starter_melee_weapons = ('Plasma Orb',)
classification = ItemClassification.filler
item = Item(name, classification, data.code, self.player)
local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items)
if not local_starter_spells:
if 'Lightwall' in non_local_items:
raise Exception("Atleast one spell must be local")
else:
local_starter_spells = ('Lightwall',)
if not item.advancement:
return item
assign_starter_item(world, player, excluded_items, locked_locations, 'Tutorial: Yo Momma 1', local_starter_melee_weapons)
assign_starter_item(world, player, excluded_items, locked_locations, 'Tutorial: Yo Momma 2', local_starter_spells)
if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"):
item.classification = ItemClassification.filler
elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"):
item.classification = ItemClassification.filler
elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"):
item.classification = ItemClassification.filler
elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \
and not self.is_option_enabled("UnchainedKeys"):
item.classification = ItemClassification.filler
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
location: str, item_list: Tuple[str, ...]):
item_name = world.random.choice(item_list)
excluded_items.add(item_name)
item = create_item_with_correct_settings(world, player, item_name)
world.get_location(location, player).place_locked_item(item)
locked_locations.append(location)
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
pool: List[Item] = []
for name, data in item_table.items():
if name not in excluded_items:
for _ in range(data.count):
item = create_item_with_correct_settings(world, player, name)
pool.append(item)
return pool
def fill_item_pool_with_dummy_items(self: TimespinnerWorld, world: MultiWorld, player: int, locked_locations: List[str],
location_cache: List[Location], pool: List[Item]):
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
item = create_item_with_correct_settings(world, player, self.get_filler_item_name())
pool.append(item)
def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item:
data = item_table[name]
if data.useful:
classification = ItemClassification.useful
elif data.progression:
classification = ItemClassification.progression
elif data.trap:
classification = ItemClassification.trap
else:
classification = ItemClassification.filler
item = Item(name, classification, data.code, player)
if not item.advancement:
return item
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
item.classification = ItemClassification.filler
elif name == 'Oculus Ring' and not is_option_enabled(world, player, "EyeSpy"):
item.classification = ItemClassification.filler
elif (name == 'Kobo' or name == 'Merchant Crow') and not is_option_enabled(world, player, "GyreArchives"):
item.classification = ItemClassification.filler
elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \
and not is_option_enabled(world, player, "UnchainedKeys"):
item.classification = ItemClassification.filler
def get_filler_item_name(self) -> str:
trap_chance: int = self.get_option_value("TrapChance")
enabled_traps: List[str] = self.get_option_value("Traps")
return item
if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps:
return self.multiworld.random.choice(enabled_traps)
else:
return self.multiworld.random.choice(filler_items)
def get_excluded_items(self) -> Set[str]:
excluded_items: Set[str] = set()
def setup_events(player: int, locked_locations: List[str], location_cache: List[Location]):
for location in location_cache:
if location.address == EventId:
item = Item(location.name, ItemClassification.progression, EventId, player)
if self.is_option_enabled("StartWithJewelryBox"):
excluded_items.add('Jewelry Box')
if self.is_option_enabled("StartWithMeyef"):
excluded_items.add('Meyef')
if self.is_option_enabled("QuickSeed"):
excluded_items.add('Talaria Attachment')
locked_locations.append(location.name)
if self.is_option_enabled("UnchainedKeys"):
excluded_items.add('Twin Pyramid Key')
location.place_locked_item(item)
if not self.is_option_enabled("EnterSandman"):
excluded_items.add('Mysterious Warp Beacon')
else:
excluded_items.add('Timeworn Warp Beacon')
excluded_items.add('Modern Warp Beacon')
excluded_items.add('Mysterious Warp Beacon')
for item in self.multiworld.precollected_items[self.player]:
if item.name not in self.item_name_groups['UseItem']:
excluded_items.add(item.name)
def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]:
personal_items: Dict[int, int] = {}
return excluded_items
for location in locations:
if location.address and location.item and location.item.code and location.item.player == player:
personal_items[location.address] = location.item.code
def assign_starter_items(self, excluded_items: Set[str]) -> None:
non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value
return personal_items
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items)
if not local_starter_melee_weapons:
if 'Plasma Orb' in non_local_items:
raise Exception("Atleast one melee orb must be local")
else:
local_starter_melee_weapons = ('Plasma Orb',)
local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items)
if not local_starter_spells:
if 'Lightwall' in non_local_items:
raise Exception("Atleast one spell must be local")
else:
local_starter_spells = ('Lightwall',)
self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 1', local_starter_melee_weapons)
self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells)
def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None:
item_name = self.multiworld.random.choice(item_list)
excluded_items.add(item_name)
item = self.create_item(item_name)
self.multiworld.get_location(location, self.player).place_locked_item(item)
def get_item_pool(self, excluded_items: Set[str]) -> List[Item]:
pool: List[Item] = []
for name, data in item_table.items():
if name not in excluded_items:
for _ in range(data.count):
item = self.create_item(name)
pool.append(item)
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool)):
item = self.create_item(self.get_filler_item_name())
pool.append(item)
return pool
def create_and_assign_event_items(self) -> None:
for location in self.multiworld.get_locations(self.player):
if location.address == EventId:
item = Item(location.name, ItemClassification.progression, EventId, self.player)
location.place_locked_item(item)
def get_personal_items(self) -> Dict[int, int]:
personal_items: Dict[int, int] = {}
for location in self.multiworld.get_locations(self.player):
if location.address and location.item and location.item.code and location.item.player == self.player:
personal_items[location.address] = location.item.code
return personal_items
def is_option_enabled(self, option: str) -> bool:
return is_option_enabled(self.multiworld, self.player, option)
def get_option_value(self, option: str) -> Union[int, Dict, List]:
return get_option_value(self.multiworld, self.player, option)