forked from mirror/Archipelago
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
1129 lines
52 KiB
Python
1129 lines
52 KiB
Python
import logging
|
|
from collections import Counter
|
|
from math import ceil
|
|
import time
|
|
|
|
from Options import OptionError
|
|
import typing
|
|
from typing import Dict, Any, List, Set
|
|
import warnings, settings
|
|
from dataclasses import asdict
|
|
|
|
from .Hints import HintData, choose_hinted_locations, generate_hint_data
|
|
from .Items import BanjoTooieItem, ItemData, all_item_table, all_group_table, progressive_ability_breakdown
|
|
from .Locations import LocationData, all_location_table, MTLoc_Table, GMLoc_table, WWLoc_table, \
|
|
JRLoc_table, TLLoc_table, GILoc_table, HPLoc_table, CCLoc_table, MumboTokenGames_table, \
|
|
MumboTokenBoss_table, MumboTokenJinjo_table, SMLoc_table, JVLoc_table, IHWHLoc_table, \
|
|
IHPLLoc_table, IHPGLoc_table, IHCTLoc_table, IHWLLoc_table, IHQMLoc_table, \
|
|
CheatoRewardsLoc_table, JinjoRewardsLoc_table, HoneyBRewardsLoc_table
|
|
from .Regions import create_regions, connect_regions
|
|
from .Options import BanjoTooieOptions, EggsBehaviour, JamjarsSiloCosts, LogicType, ProgressiveEggAim, \
|
|
ProgressiveWaterTraining, RandomizeBKMoveList, VictoryCondition, bt_option_groups, WorldRequirements
|
|
from .Rules import BanjoTooieRules
|
|
from .Names import itemName, locationName, regionName
|
|
from .WorldOrder import randomize_world_progression
|
|
from BaseClasses import ItemClassification, Location, MultiWorld, Tutorial, Item
|
|
from worlds.AutoWorld import World, WebWorld
|
|
from worlds.LauncherComponents import Component, icon_paths, components, Type, launch_subprocess
|
|
|
|
|
|
def run_client():
|
|
from .BTClient import main # lazy import
|
|
launch_subprocess(main)
|
|
|
|
|
|
components.append(Component("Banjo-Tooie Client", func=run_client, component_type=Type.CLIENT,
|
|
icon='Jinjo Icon'))
|
|
icon_paths['Jinjo Icon'] = "ap:worlds.banjo_tooie/assets/icon.png"
|
|
|
|
class BanjoTooieSettings(settings.Group):
|
|
class RomPath(settings.OptionalUserFilePath):
|
|
"""File path of the Banjo-Tooie (USA) ROM."""
|
|
|
|
class PatchPath(settings.OptionalUserFolderPath):
|
|
"""Folder path of where to save the patched ROM."""
|
|
|
|
class ProgramPath(settings.OptionalUserFilePath):
|
|
"""
|
|
File path of the program to automatically run.
|
|
Leave blank to disable.
|
|
"""
|
|
|
|
class ProgramArgs(str):
|
|
"""
|
|
Arguments to pass to the automatically run program.
|
|
Leave blank to disable.
|
|
Set to "--lua=" to automatically use the correct path for the lua connector.
|
|
"""
|
|
|
|
rom_path: RomPath | str = ""
|
|
patch_path: PatchPath | str = ""
|
|
program_path: ProgramPath | str = ""
|
|
program_args: ProgramArgs | str = "--lua="
|
|
|
|
|
|
class BanjoTooieWeb(WebWorld):
|
|
setup_en = Tutorial(
|
|
"Setup Banjo-Tooie",
|
|
"""A guide to setting up Archipelago Banjo-Tooie on your computer.""",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Beebaleen"])
|
|
setup_fr = Tutorial(
|
|
"Setup Banjo-Tooie",
|
|
"""A guide to setting up Archipelago Banjo-Tooie on your computer.""",
|
|
"French",
|
|
"setup_fr.md",
|
|
"setup/fr",
|
|
["g0goTBC"])
|
|
|
|
tutorials = [setup_en, setup_fr]
|
|
option_groups = bt_option_groups
|
|
|
|
|
|
class BanjoTooieWorld(World):
|
|
"""
|
|
Banjo-Tooie is a single-player platform game in which the protagonists are controlled from a third-person perspective.
|
|
Carrying over most of the mechanics and concepts established in its predecessor,
|
|
the game features three-dimensional worlds consisting of various platforming challenges and puzzles, with a notable
|
|
increased focus on puzzle-solving over the worlds of Banjo-Kazooie.
|
|
"""
|
|
|
|
game = "Banjo-Tooie"
|
|
options: BanjoTooieOptions
|
|
settings: typing.ClassVar[BanjoTooieSettings]
|
|
settings_key = "banjo_tooie_options"
|
|
web = BanjoTooieWeb()
|
|
topology_present = True
|
|
item_name_to_id = {name: data.btid for name, data in all_item_table.items() if data.btid is not None}
|
|
|
|
glitches_item_name = itemName.UT_GLITCHED
|
|
ut_can_gen_without_yaml = True
|
|
|
|
location_name_to_id = {name: data.btid for name, data in all_location_table.items() if data.btid is not None}
|
|
location_name_to_group = {name: data.group for name, data in all_location_table.items() if data.group is not None}
|
|
|
|
item_name_groups = {
|
|
# "Jiggy": all_group_table["jiggy"],
|
|
"Jinjo": set(all_group_table["jinjo"].keys()),
|
|
"Moves": set(all_group_table["moves"].keys()),
|
|
"Magic": set(all_group_table["magic"].keys()),
|
|
"Stations": set(all_group_table["stations"].keys()),
|
|
"StopnSwap": set(all_group_table["stopnswap"].keys()),
|
|
"Access": set(all_group_table["levelaccess"].keys()),
|
|
"Dino": set(all_group_table["dino"].keys()),
|
|
"Silos": set(all_group_table["Silos"].keys()),
|
|
"Warp Pads": set(all_group_table["Warp Pads"].keys()),
|
|
"Cheats": set(all_group_table["cheats"].keys())
|
|
}
|
|
|
|
location_name_groups = {
|
|
"Mayahem Temple": set(MTLoc_Table.keys()), "Glitter Gulch Mine": set(GMLoc_table.keys()),
|
|
"Witchyworld": set(WWLoc_table.keys()), "Jolly Roger's Lagoon": set(JRLoc_table.keys()),
|
|
"Terrydactyland": set(TLLoc_table.keys()), "Grunty Industries": set(GILoc_table.keys()),
|
|
"Hailfire Peaks": set(HPLoc_table.keys()), "Cloud Cuckooland": set(CCLoc_table.keys()),
|
|
"Isle O' Hags": set(SMLoc_table.keys()) | set(JVLoc_table.keys()) | set(IHWHLoc_table.keys()) | set(IHPLLoc_table.keys()) |
|
|
set(IHPGLoc_table.keys()) | set(IHCTLoc_table.keys()) | set(IHWLLoc_table.keys()) | set(IHQMLoc_table.keys()),
|
|
"Cheato Rewards": set(CheatoRewardsLoc_table.keys()),
|
|
"Jinjo Rewards": set(JinjoRewardsLoc_table.keys()),
|
|
"Honey B Rewards": set(HoneyBRewardsLoc_table.keys()),
|
|
"Jiggies": {c for c in all_location_table if all_location_table[c].group == "Jiggy"},
|
|
"Jinjos": {c for c in all_location_table if all_location_table[c].group == "Jinjo"},
|
|
"Empty Honeycombs": {c for c in all_location_table if
|
|
all_location_table[c].group == "Honeycomb"},
|
|
"Cheato Pages": {c for c in all_location_table if
|
|
all_location_table[c].group == "Cheato Page"},
|
|
"Notes": {c for c in all_location_table if all_location_table[c].group == "Note"},
|
|
"Treble Clefs": {c for c in all_location_table if
|
|
all_location_table[c].group == "Treble Clef"},
|
|
"Doubloons": {c for c in JRLoc_table if JRLoc_table[c].group == "Doubloon"},
|
|
"Signposts": {c for c in all_location_table if all_location_table[c].group == "Signpost"},
|
|
"Jamjars Silos": {c for c in all_location_table if
|
|
all_location_table[c].group == "Jamjars Silo"},
|
|
"Glowbos": {c for c in all_location_table if all_location_table[c].group == "Glowbo"},
|
|
"Train Switches": {c for c in all_location_table if
|
|
all_location_table[c].group == "Train Switch"},
|
|
"Stop 'n' Swop": {c for c in all_location_table if
|
|
all_location_table[c].group == "Stop 'n' Swop"},
|
|
"Nests": {c for c in all_location_table if all_location_table[c].group == "Nest"},
|
|
"Warp Pads": {c for c in all_location_table if all_location_table[c].group == "Warp Pads"},
|
|
"Warp Silos": {c for c in all_location_table if all_location_table[c].group == "Silos"},
|
|
"Ticket": {c for c in all_location_table if all_location_table[c].group == "Ticket"},
|
|
"Green Relic": {c for c in all_location_table if
|
|
all_location_table[c].group == "Green Relic"}, "Bosses": {
|
|
locationName.JIGGYMT1,
|
|
locationName.JIGGYGM1,
|
|
locationName.JIGGYWW3,
|
|
locationName.JIGGYJR7,
|
|
locationName.JIGGYTD1,
|
|
locationName.JIGGYTD4,
|
|
locationName.JIGGYGI2,
|
|
locationName.CHEATOGI3,
|
|
locationName.JIGGYHP1,
|
|
locationName.JIGGYCC1,
|
|
}, "Minigames": {
|
|
locationName.JIGGYMT3,
|
|
locationName.JIGGYGM2,
|
|
locationName.JIGGYGM5,
|
|
locationName.JIGGYWW1,
|
|
locationName.JIGGYWW2,
|
|
locationName.JIGGYWW4,
|
|
locationName.JIGGYWW5,
|
|
locationName.JIGGYJR1,
|
|
locationName.JIGGYTD6,
|
|
locationName.JIGGYGI9,
|
|
locationName.JIGGYHP8,
|
|
locationName.JIGGYCC3,
|
|
locationName.JIGGYCC4,
|
|
locationName.JIGGYCC5,
|
|
locationName.JIGGYCC8,
|
|
locationName.CHEATOGM1,
|
|
locationName.CHEATOWW3,
|
|
locationName.CHEATOCC2,
|
|
locationName.CHEATOCC1,
|
|
locationName.CHEATOCC3,
|
|
}}
|
|
|
|
options_dataclass = BanjoTooieOptions
|
|
options: BanjoTooieOptions
|
|
|
|
has_converted_progression_items = False
|
|
|
|
def __init__(self, world, player):
|
|
self.starting_egg: int = 0
|
|
self.starting_attack: int = 0
|
|
|
|
self.hard_item_limit: int = 250
|
|
self.traps_in_pool: int = 0
|
|
self.jiggies_in_pool: int = 0
|
|
self.notes_in_pool: int = 0
|
|
self.doubloons_in_pool: int = 0
|
|
|
|
self.slot_data = []
|
|
self.preopened_silos = []
|
|
self.world_requirements = {}
|
|
self.world_order = {}
|
|
self.loading_zones = {}
|
|
self.jamjars_siloname_costs = {}
|
|
self.jamjars_silo_costs = {}
|
|
|
|
self.hinted_locations: Set[Location] = set()
|
|
self.hints: dict[int, HintData] = {}
|
|
|
|
super(BanjoTooieWorld, self).__init__(world, player)
|
|
|
|
def create_item(self, name: str) -> Item:
|
|
item_classification = None
|
|
|
|
if name == itemName.JIGGY_AS_USEFUL:
|
|
name = itemName.JIGGY
|
|
if not hasattr(self.multiworld, "generation_is_fake"):
|
|
item_classification = ItemClassification.useful
|
|
elif name == itemName.NOTE_AS_USEFUL:
|
|
name = itemName.NOTE
|
|
if not hasattr(self.multiworld, "generation_is_fake"):
|
|
item_classification = ItemClassification.useful
|
|
elif name == itemName.DOUBLOON_AS_USEFUL:
|
|
name = itemName.DOUBLOON
|
|
if not hasattr(self.multiworld, "generation_is_fake"):
|
|
item_classification = ItemClassification.useful
|
|
elif name == itemName.HEALTHUP and (
|
|
self.options.logic_type.value
|
|
== LogicType.option_easy_tricks or self.options.logic_type.value == LogicType.option_intended
|
|
):
|
|
item_classification = ItemClassification.useful
|
|
|
|
banjoItem = all_item_table.get(name)
|
|
if not banjoItem:
|
|
raise ValueError(f"{name} is not a valid item name for Banjo-Tooie")
|
|
|
|
if item_classification is None:
|
|
item_classification = self.get_classification(banjoItem)
|
|
|
|
if item_classification == ItemClassification.trap:
|
|
self.traps_in_pool += 1
|
|
|
|
if name == itemName.JIGGY:
|
|
self.jiggies_in_pool += 1
|
|
if name == itemName.NOTE:
|
|
self.notes_in_pool += 1
|
|
if name == itemName.DOUBLOON:
|
|
self.doubloons_in_pool += 1
|
|
|
|
created_item = BanjoTooieItem(name, item_classification, banjoItem.btid, self.player)
|
|
return created_item
|
|
|
|
def get_classification(self, banjoItem: ItemData) -> ItemClassification:
|
|
if banjoItem.btid is not None:
|
|
itemname = self.item_id_to_name[banjoItem.btid]
|
|
|
|
if itemname == itemName.PAGES:
|
|
if self.options.cheato_rewards.value:
|
|
return ItemClassification.progression_deprioritized_skip_balancing
|
|
else:
|
|
return ItemClassification.filler
|
|
|
|
if itemname == itemName.HONEY:
|
|
if self.options.honeyb_rewards.value:
|
|
return ItemClassification.progression_deprioritized_skip_balancing
|
|
else:
|
|
return ItemClassification.useful
|
|
|
|
if banjoItem.type not in (
|
|
ItemClassification.progression,
|
|
ItemClassification.progression_deprioritized_skip_balancing,
|
|
ItemClassification.useful,
|
|
ItemClassification.filler,
|
|
ItemClassification.trap
|
|
):
|
|
raise Exception(f"{banjoItem.type} does not correspond to a valid item classification.")
|
|
|
|
return banjoItem.type
|
|
|
|
def create_event_item(self, name: str) -> Item:
|
|
item_classification = ItemClassification.progression
|
|
created_item = BanjoTooieItem(name, item_classification, None, self.player)
|
|
return created_item
|
|
|
|
def get_jiggies_in_pool(self) -> List[Item]:
|
|
itempool = []
|
|
|
|
if self.options.jingaling_jiggy.value:
|
|
# Below give the king a guarentee Jiggy if option is set
|
|
self.get_location(locationName.JIGGYIH10).place_locked_item(self.create_item(itemName.JIGGY))
|
|
|
|
last_level_requirement = max(self.world_requirements.values())
|
|
if not self.options.open_hag1.value and self.options.victory_condition.value == VictoryCondition.option_hag1:
|
|
last_level_requirement = max(last_level_requirement, 70)
|
|
|
|
# Buffer of 5 progression so that cryptic hints do not consider every jiggy as required,
|
|
# and so that the spoiler log does not list the absolute worst jiggies as
|
|
# part of the playthrough.
|
|
progression_jiggies = min(last_level_requirement + 5, 90)
|
|
|
|
# Buffer that is not considered in logic to make the generation faster while making the seed easier.
|
|
useful_jiggies = ceil((90 - progression_jiggies - 5)/2)\
|
|
if self.options.replace_extra_jiggies.value\
|
|
else 90 - progression_jiggies
|
|
|
|
# Some progression jiggies can be placed as locked items, so we don't add them to the pool.
|
|
if self.options.jingaling_jiggy.value:
|
|
progression_jiggies -= 1
|
|
if not self.options.randomize_jinjos.value:
|
|
progression_jiggies -= 9
|
|
|
|
# in case preplaced items are over the progression count
|
|
if progression_jiggies < 0:
|
|
useful_jiggies += progression_jiggies
|
|
progression_jiggies = 0
|
|
|
|
itempool += [
|
|
self.create_item(itemName.JIGGY) for i in range(progression_jiggies)
|
|
]
|
|
itempool += [
|
|
self.create_item(itemName.JIGGY_AS_USEFUL) for i in range(useful_jiggies)
|
|
]
|
|
return itempool
|
|
|
|
def get_notes_in_pool(self) -> List[Item]:
|
|
if not self.options.randomize_notes.value:
|
|
return []
|
|
|
|
itempool = []
|
|
|
|
progression_notes = ceil(max(self.jamjars_siloname_costs.values()) / 5)
|
|
|
|
taken_by_clefs = 4 * (self.options.extra_trebleclefs_count.value + all_item_table[itemName.TREBLE].qty)\
|
|
+ 2 * self.options.bass_clef_amount.value
|
|
|
|
progression_notes -= taken_by_clefs
|
|
# Random Jamjars costs can make max silo very low (even 0); treble deduction still applies to the
|
|
# 900-note budget but cannot require negative progression bundles — clamp here (no bug).
|
|
if progression_notes < 0:
|
|
progression_notes = 0
|
|
|
|
# Treble clef locations always remove 20 notes each from the 900-note budget, whether clefs are
|
|
# randomized (in the item pool) or vanilla (prefilled). Using 0 here when trebles are off made
|
|
# note_item_slots too large (180 bundles vs 144 note nests), overflowing the item pool.
|
|
treble_items = all_item_table[itemName.TREBLE].qty + self.options.extra_trebleclefs_count.value
|
|
bass_items = self.options.bass_clef_amount.value
|
|
raw_notes_from_clefs = 20 * treble_items + 10 * bass_items
|
|
note_item_slots = (900 - raw_notes_from_clefs) // 5
|
|
if note_item_slots < 0:
|
|
logging.warning("Clefs exceed 900 notes worth; no 5-note bundles will be added.")
|
|
note_item_slots = 0
|
|
|
|
progression_notes = min(progression_notes, note_item_slots)
|
|
remainder = note_item_slots - progression_notes
|
|
|
|
if self.options.replace_extra_notes.value:
|
|
useful_note_bundles = ceil(remainder / 2)
|
|
else:
|
|
useful_note_bundles = remainder
|
|
|
|
itempool += [
|
|
self.create_item(itemName.NOTE) for i in range(progression_notes)
|
|
]
|
|
itempool += [
|
|
self.create_item(itemName.NOTE_AS_USEFUL) for i in range(useful_note_bundles)
|
|
]
|
|
|
|
return itempool
|
|
|
|
def create_items(self) -> None:
|
|
itempool = []
|
|
|
|
# START OF ITEMS CUSTOM LOGIC
|
|
|
|
if self.options.victory_condition.value == VictoryCondition.option_token_hunt:
|
|
itempool += [self.create_item(itemName.MUMBOTOKEN) for i in range(self.options.tokens_in_pool.value)]
|
|
|
|
itempool += self.get_jiggies_in_pool()
|
|
itempool += self.get_notes_in_pool()
|
|
|
|
count = all_item_table[itemName.TREBLE].qty if self.options.randomize_treble.value else 0
|
|
count += self.options.extra_trebleclefs_count
|
|
itempool += [self.create_item(itemName.TREBLE) for i in range(count)]
|
|
|
|
count = self.options.bass_clef_amount.value
|
|
itempool += [self.create_item(itemName.BASS) for i in range(count)]
|
|
|
|
# END OF ITEMS CUSTOM LOGIC
|
|
|
|
# Basic items that need no extra logic, if you need to customize quantity or logic, add them above this
|
|
# and add the item to the handled_items in def item_filter.
|
|
for name, item in all_item_table.items():
|
|
item_name = self.item_filter(name, item)
|
|
if item_name is not None:
|
|
# We're still using the original item quantity.
|
|
itempool += [self.create_item(item_name) for _ in range(item.qty)]
|
|
|
|
# Add Filler items until all locations are filled
|
|
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
|
if len(itempool) > total_locations:
|
|
warnings.warn(
|
|
"Number of total available items exceeds the number of locations,\
|
|
likely there is a bug in the generation."
|
|
)
|
|
|
|
itempool += [self.create_filler() for _ in range(total_locations - len(itempool))]
|
|
|
|
self.multiworld.itempool.extend(itempool)
|
|
|
|
def item_filter(self, name: str, item: ItemData) -> str | None:
|
|
handled_items = [
|
|
itemName.JIGGY,
|
|
itemName.NOTE,
|
|
itemName.TREBLE,
|
|
itemName.BASS,
|
|
itemName.MUMBOTOKEN,
|
|
]
|
|
|
|
if name in handled_items:
|
|
return None
|
|
|
|
# While JNONE is filler, it's funny enough to warrant always keeping
|
|
if item.type in (ItemClassification.filler, ItemClassification.trap) and name != itemName.JNONE:
|
|
return None
|
|
|
|
if name == itemName.DOUBLOON and not self.options.randomize_doubloons.value:
|
|
return None
|
|
|
|
if name == itemName.PAGES and not self.options.randomize_cheato.value: # Added later in Prefill
|
|
return None
|
|
|
|
if name == itemName.HONEY and not self.options.randomize_honeycombs.value: # Added later in Prefill
|
|
return None
|
|
|
|
if name == itemName.HEALTHUP and not self.options.honeyb_rewards.value:
|
|
return None
|
|
|
|
if name in all_group_table['bk_moves'].keys()\
|
|
and self.options.randomize_bk_moves.value == RandomizeBKMoveList.option_none:
|
|
return None
|
|
|
|
# talon trot and tall jump not in pool
|
|
elif (name == itemName.TTROT or name == itemName.TJUMP)\
|
|
and self.options.randomize_bk_moves.value == RandomizeBKMoveList.option_mcjiggy_special:
|
|
return None
|
|
|
|
if name in all_group_table['moves'].keys() and not self.options.randomize_bt_moves.value:
|
|
return None
|
|
|
|
if name in all_group_table['magic'].keys() and not self.options.randomize_glowbos.value:
|
|
return None
|
|
|
|
if name in all_group_table['jinjo'].keys() and not self.options.randomize_jinjos.value:
|
|
return None
|
|
|
|
if name == itemName.CHUFFY and not self.options.randomize_chuffy.value:
|
|
return None
|
|
|
|
if name in all_group_table['stations'].keys() and not self.options.randomize_stations.value:
|
|
return None
|
|
|
|
if name in all_group_table['levelaccess'].keys():
|
|
return None
|
|
|
|
if name in all_group_table['stopnswap'].keys() and not self.options.randomize_stop_n_swap.value:
|
|
return None
|
|
|
|
if name in all_group_table['Warp Pads'].keys() and not self.options.randomize_warp_pads.value:
|
|
return None
|
|
|
|
if name in all_group_table['Silos'].keys() and not self.options.randomize_silos.value:
|
|
return None
|
|
|
|
if name in all_group_table['cheats'].keys() and not self.options.cheato_rewards.value:
|
|
return None
|
|
|
|
if name in all_group_table['Silos'].keys() and name in self.preopened_silos:
|
|
return None
|
|
|
|
if name == itemName.ROAR and not self.options.randomize_dino_roar.value:
|
|
return None
|
|
|
|
if name == itemName.GRRELIC and not self.options.randomize_green_relics.value:
|
|
return None
|
|
|
|
if name == itemName.BTTICKET and not self.options.randomize_tickets.value:
|
|
return None
|
|
|
|
if name == itemName.BEANS and not self.options.randomize_beans.value:
|
|
return None
|
|
|
|
if item.btid == self.starting_egg:
|
|
return None
|
|
|
|
if item.btid == self.starting_attack:
|
|
return None
|
|
elif self.starting_attack != 0: # Let's check if it's progressive starting move
|
|
attack_name = self.item_id_to_name[self.starting_attack]
|
|
if attack_name in progressive_ability_breakdown.keys() and \
|
|
name == progressive_ability_breakdown[attack_name][0]:
|
|
return None
|
|
|
|
# START OF PROGRESSIVE MOVES
|
|
|
|
# We add a progressive ability when we go through the individual items
|
|
if name in progressive_ability_breakdown.keys():
|
|
return None
|
|
|
|
if self.options.progressive_beak_buster.value:
|
|
if name in progressive_ability_breakdown[itemName.PBBUST]:
|
|
return itemName.PBBUST
|
|
|
|
if self.options.egg_behaviour.value == EggsBehaviour.option_progressive_eggs:
|
|
if name in progressive_ability_breakdown[itemName.PEGGS]:
|
|
return itemName.PEGGS
|
|
|
|
if self.options.progressive_shoes.value:
|
|
if name in progressive_ability_breakdown[itemName.PSHOES]:
|
|
return itemName.PSHOES
|
|
|
|
if self.options.progressive_water_training.value == ProgressiveWaterTraining.option_basic:
|
|
if name in progressive_ability_breakdown[itemName.PSWIM]:
|
|
return itemName.PSWIM
|
|
elif self.options.progressive_water_training.value == ProgressiveWaterTraining.option_advanced:
|
|
if name in progressive_ability_breakdown[itemName.PASWIM]:
|
|
return itemName.PASWIM
|
|
|
|
if self.options.progressive_bash_attack.value:
|
|
if name in progressive_ability_breakdown[itemName.PBASH]:
|
|
return itemName.PBASH
|
|
|
|
if self.options.progressive_flight.value:
|
|
if name in progressive_ability_breakdown[itemName.PFLIGHT]:
|
|
return itemName.PFLIGHT
|
|
|
|
if self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_basic:
|
|
if name in progressive_ability_breakdown[itemName.PEGGAIM]:
|
|
return itemName.PEGGAIM
|
|
elif self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_advanced:
|
|
if name in progressive_ability_breakdown[itemName.PAEGGAIM]:
|
|
return itemName.PAEGGAIM
|
|
|
|
# END OF PROGRESSIVE MOVES
|
|
|
|
return name
|
|
|
|
def create_regions(self) -> None:
|
|
create_regions(self)
|
|
connect_regions(self)
|
|
self.pre_fill_me()
|
|
|
|
def generate_early(self) -> None:
|
|
re_gen_passthrough = getattr(self.multiworld, "re_gen_passthrough", {})
|
|
if re_gen_passthrough and self.game in re_gen_passthrough:
|
|
# Universal Tracker trickery
|
|
slot_data = self.multiworld.re_gen_passthrough[self.game]
|
|
|
|
slot_options: dict[str, Any] = slot_data.get("options", {})
|
|
for key, value in slot_options.items():
|
|
opt = getattr(self.options, key, None)
|
|
if opt is not None:
|
|
setattr(self.options, key, opt.from_any(value))
|
|
|
|
custom_bt_data = slot_data["custom_bt_data"]
|
|
self.world_requirements = custom_bt_data["world_requirements"]
|
|
self.world_order = custom_bt_data["world_order"]
|
|
self.jamjars_siloname_costs = custom_bt_data["jamjars_siloname_costs"]
|
|
self.loading_zones = custom_bt_data["loading_zones"]
|
|
else:
|
|
# Normal generation
|
|
self.validate_yaml_options()
|
|
self.choose_starter_egg()
|
|
self.choose_starter_attack()
|
|
randomize_world_progression(self)
|
|
self.hand_preopened_silos()
|
|
|
|
def validate_yaml_options(self) -> None:
|
|
if self.options.randomize_worlds.value \
|
|
and self.options.randomize_bk_moves.value != RandomizeBKMoveList.option_none\
|
|
and self.options.logic_type.value == LogicType.option_intended:
|
|
raise OptionError("Randomize Worlds and Randomize BK Moves is not compatible with Intended Logic.")
|
|
if not self.options.randomize_notes.value \
|
|
and not self.options.randomize_signposts.value and not self.options.nestsanity.value \
|
|
and self.options.randomize_bk_moves.value != RandomizeBKMoveList.option_none:
|
|
raise OptionError("Your options are too restrictive for Randomize BK Moves."
|
|
"Try randomizing notes, signs or nestsanity")
|
|
if self.options.victory_condition.value == VictoryCondition.option_token_hunt:
|
|
if self.options.token_hunt_length.value > self.options.tokens_in_pool.value:
|
|
self.options.token_hunt_length.value = self.options.tokens_in_pool.value
|
|
if self.options.tokens_in_pool.value > 15\
|
|
and not self.options.randomize_signposts.value\
|
|
and not self.options.nestsanity.value:
|
|
raise OptionError(
|
|
"You cannot have more than 15 Mumbo Tokens without enabling Randomize Signposts or Nestanity."
|
|
)
|
|
if self.options.tokens_in_pool.value > 50 and not self.options.nestsanity.value:
|
|
raise OptionError("You cannot have more than 50 Mumbo Tokens without enabling Nestanity.")
|
|
if not self.options.randomize_notes.value\
|
|
and self.options.extra_trebleclefs_count.value != 0\
|
|
and self.options.bass_clef_amount.value != 0:
|
|
raise OptionError("Randomize Notes is required to add extra Treble Clefs or Bass Clefs")
|
|
if self.options.progressive_beak_buster.value\
|
|
and (not self.options.randomize_bk_moves.value or not self.options.randomize_bt_moves.value):
|
|
raise OptionError(
|
|
"You cannot have progressive Beak Buster without randomizing moves and randomizing BK moves"
|
|
)
|
|
if (self.options.egg_behaviour.value == EggsBehaviour.option_random_starting_egg
|
|
or self.options.egg_behaviour.value == EggsBehaviour.option_simple_random_starting_egg) \
|
|
and (not self.options.randomize_bk_moves.value or not self.options.randomize_bt_moves.value):
|
|
raise OptionError(
|
|
"You cannot have Randomize Starting Egg without randomizing moves and randomizing BK moves"
|
|
)
|
|
elif self.options.egg_behaviour.value == EggsBehaviour.option_progressive_eggs\
|
|
and not self.options.randomize_bt_moves.value:
|
|
raise OptionError("You cannot have progressive Eggs without randomizing moves")
|
|
if self.options.progressive_shoes.value\
|
|
and not (
|
|
self.options.randomize_bk_moves.value
|
|
and self.options.randomize_bt_moves.value
|
|
):
|
|
raise OptionError("You cannot have progressive Shoes without randomizing moves, or "
|
|
"randomizing BK moves")
|
|
if self.options.progressive_water_training.value != ProgressiveWaterTraining.option_none \
|
|
and (
|
|
self.options.randomize_bk_moves.value == RandomizeBKMoveList.option_none
|
|
or not self.options.randomize_bt_moves.value
|
|
):
|
|
raise OptionError(
|
|
"You cannot have progressive Water Training without randomizing moves and randomizing BK moves"
|
|
)
|
|
if self.options.progressive_flight.value\
|
|
and (not self.options.randomize_bk_moves.value or not self.options.randomize_bt_moves.value):
|
|
raise OptionError("You cannot have progressive flight without randomizing moves and randomizing BK moves")
|
|
if self.options.progressive_egg_aiming.value != ProgressiveEggAim.option_none\
|
|
and (not self.options.randomize_bk_moves.value or not self.options.randomize_bt_moves.value):
|
|
raise OptionError(
|
|
"You cannot have progressive egg aiming without randomizing moves and randomizing BK moves"
|
|
)
|
|
if self.options.progressive_bash_attack.value\
|
|
and (not self.options.randomize_stop_n_swap.value or not self.options.randomize_bt_moves.value):
|
|
raise OptionError(
|
|
"You cannot have progressive bash attack without randomizing Stop N Swap and randomizing BK moves"
|
|
)
|
|
if not self.options.randomize_bt_moves.value and self.options.jamjars_silo_costs.value != JamjarsSiloCosts.option_vanilla:
|
|
raise OptionError("You cannot change the silo costs without randomizing Jamjars' moves.")
|
|
if not self.options.open_hag1.value\
|
|
and self.options.victory_condition.value == VictoryCondition.option_wonderwing_challenge:
|
|
self.options.open_hag1.value = True
|
|
if self.options.world_requirements.value != WorldRequirements.option_normal and not self.options.skip_puzzles.value:
|
|
raise OptionError("Your world requirements needs to be set to normal if you are not going to skip puzzles.")
|
|
|
|
def choose_starter_egg(self) -> None:
|
|
if self.options.egg_behaviour.value == EggsBehaviour.option_random_starting_egg or \
|
|
self.options.egg_behaviour.value == EggsBehaviour.option_simple_random_starting_egg:
|
|
|
|
if self.options.egg_behaviour.value == EggsBehaviour.option_random_starting_egg:
|
|
eggs = [itemName.BEGGS, itemName.FEGGS, itemName.GEGGS, itemName.IEGGS, itemName.CEGGS]
|
|
else:
|
|
eggs = [itemName.BEGGS, itemName.FEGGS, itemName.GEGGS, itemName.IEGGS]
|
|
egg_name = self.random.choice(eggs)
|
|
starting_egg = self.create_item(egg_name)
|
|
self.multiworld.push_precollected(starting_egg)
|
|
banjoItem = all_item_table[egg_name]
|
|
self.starting_egg = banjoItem.btid
|
|
else:
|
|
starting_egg = self.create_item(itemName.BEGGS)
|
|
self.multiworld.push_precollected(starting_egg)
|
|
banjoItem = all_item_table[itemName.BEGGS]
|
|
self.starting_egg = banjoItem.btid
|
|
|
|
def choose_starter_attack(self) -> None:
|
|
if self.options.randomize_bk_moves.value != RandomizeBKMoveList.option_none:
|
|
if self.options.logic_type.value == LogicType.option_intended:
|
|
if self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_basic:
|
|
base_attacks = [itemName.PEGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT]
|
|
elif self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_advanced:
|
|
base_attacks = [itemName.PAEGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT]
|
|
else:
|
|
base_attacks = [itemName.EGGSHOOT, itemName.EGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT]
|
|
elif self.options.logic_type.value == LogicType.option_easy_tricks:
|
|
if self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_basic:
|
|
base_attacks = [itemName.PEGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT, itemName.WWING]
|
|
elif self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_advanced:
|
|
base_attacks = [itemName.PAEGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT, itemName.WWING]
|
|
else:
|
|
base_attacks = [
|
|
itemName.EGGSHOOT,
|
|
itemName.EGGAIM,
|
|
itemName.BBARGE,
|
|
itemName.ROLL,
|
|
itemName.ARAT,
|
|
itemName.WWING
|
|
]
|
|
else:
|
|
if self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_basic:
|
|
base_attacks = [itemName.PEGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT, itemName.WWING]
|
|
elif self.options.progressive_egg_aiming.value == ProgressiveEggAim.option_advanced:
|
|
base_attacks = [itemName.PAEGGAIM, itemName.BBARGE, itemName.ROLL, itemName.ARAT, itemName.WWING]
|
|
else:
|
|
base_attacks = [
|
|
itemName.EGGSHOOT,
|
|
itemName.EGGAIM,
|
|
itemName.BBARGE,
|
|
itemName.ROLL,
|
|
itemName.ARAT,
|
|
itemName.WWING
|
|
]
|
|
base_attacks.append(itemName.PBASH if self.options.progressive_bash_attack.value else itemName.GRAT)
|
|
base_attacks.append(itemName.PBBUST if self.options.progressive_beak_buster.value else itemName.BBUST)
|
|
chosen_attack = self.random.choice(base_attacks)
|
|
|
|
starting_attack = self.create_item(chosen_attack)
|
|
self.multiworld.push_precollected(starting_attack)
|
|
banjoItem = all_item_table.get(chosen_attack)
|
|
self.starting_attack = banjoItem.btid
|
|
|
|
def hand_preopened_silos(self) -> None:
|
|
for silo in self.preopened_silos:
|
|
self.multiworld.push_precollected(self.create_item(silo))
|
|
|
|
def set_rules(self) -> None:
|
|
rules = BanjoTooieRules(self)
|
|
return rules.set_rules()
|
|
|
|
def pre_fill_me(self) -> None:
|
|
def prefill_locations_with_item(item_name: str, locations: list[str]) -> None:
|
|
for location_name in locations:
|
|
self.get_location(location_name).place_locked_item(self.create_item(item_name))
|
|
|
|
if not self.options.randomize_honeycombs.value:
|
|
self.banjo_pre_fills(itemName.HONEY, "Honeycomb", False)
|
|
|
|
if not self.options.randomize_cheato.value:
|
|
self.banjo_pre_fills(itemName.PAGES, "Cheato Page", False)
|
|
|
|
if not self.options.randomize_doubloons.value:
|
|
self.banjo_pre_fills(itemName.DOUBLOON, "Doubloon", False)
|
|
|
|
if not self.options.randomize_bt_moves.value:
|
|
self.banjo_pre_fills("Moves", None, True)
|
|
|
|
if not self.options.randomize_dino_roar.value:
|
|
self.banjo_pre_fills("Dino", None, True)
|
|
|
|
if not self.options.randomize_glowbos.value:
|
|
self.banjo_pre_fills("Magic", None, True)
|
|
|
|
if not self.options.randomize_treble.value:
|
|
self.banjo_pre_fills(itemName.TREBLE, "Treble Clef", False)
|
|
|
|
if not self.options.randomize_stations.value:
|
|
self.banjo_pre_fills("Stations", None, True)
|
|
|
|
if not self.options.randomize_chuffy.value:
|
|
self.banjo_pre_fills(itemName.CHUFFY, "Chuffy", False)
|
|
|
|
if not self.options.randomize_notes.value:
|
|
self.banjo_pre_fills(itemName.NOTE, "Note", False)
|
|
|
|
if not self.options.randomize_stop_n_swap.value:
|
|
self.banjo_pre_fills("StopnSwap", None, True)
|
|
|
|
if not self.options.cheato_rewards.value:
|
|
self.banjo_pre_fills("Cheats", None, True)
|
|
|
|
if self.options.skip_puzzles.value:
|
|
world_num = 1
|
|
for world, amt in self.world_requirements.items():
|
|
if world == regionName.GIO:
|
|
item = self.create_item(itemName.GIA)
|
|
elif world == regionName.JR:
|
|
item = self.create_item(itemName.JRA)
|
|
else:
|
|
item = self.create_item(world)
|
|
self.get_location(f"World {world_num} Unlocked").place_locked_item(item)
|
|
world_num += 1
|
|
|
|
if self.options.victory_condition.value == VictoryCondition.option_minigame_hunt\
|
|
or self.options.victory_condition.value == VictoryCondition.option_wonderwing_challenge:
|
|
|
|
item = self.create_item(itemName.MUMBOTOKEN)
|
|
for location_name in MumboTokenGames_table.keys():
|
|
self.get_location(location_name).place_locked_item(item)
|
|
|
|
if self.options.victory_condition.value == VictoryCondition.option_boss_hunt \
|
|
or self.options.victory_condition.value == VictoryCondition.option_wonderwing_challenge \
|
|
or self.options.victory_condition.value == VictoryCondition.option_boss_hunt_and_hag1:
|
|
|
|
item = self.create_item(itemName.MUMBOTOKEN)
|
|
for location_name in MumboTokenBoss_table.keys():
|
|
self.get_location(location_name).place_locked_item(item)
|
|
|
|
if self.options.victory_condition.value == VictoryCondition.option_jinjo_family_rescue\
|
|
or self.options.victory_condition.value == VictoryCondition.option_wonderwing_challenge:
|
|
item = self.create_item(itemName.MUMBOTOKEN)
|
|
for location_name in MumboTokenJinjo_table.keys():
|
|
self.get_location(location_name).place_locked_item(item)
|
|
|
|
if not self.options.randomize_jinjos.value:
|
|
prefill_locations_with_item(itemName.JIGGY, [
|
|
locationName.JIGGYIH1,
|
|
locationName.JIGGYIH2,
|
|
locationName.JIGGYIH3,
|
|
locationName.JIGGYIH4,
|
|
locationName.JIGGYIH5,
|
|
locationName.JIGGYIH6,
|
|
locationName.JIGGYIH7,
|
|
locationName.JIGGYIH8,
|
|
locationName.JIGGYIH9
|
|
])
|
|
prefill_locations_with_item(itemName.WJINJO, [
|
|
locationName.JINJOJR5
|
|
])
|
|
prefill_locations_with_item(itemName.OJINJO, [
|
|
locationName.JINJOWW4,
|
|
locationName.JINJOHP2
|
|
])
|
|
prefill_locations_with_item(itemName.YJINJO, [
|
|
locationName.JINJOWW3,
|
|
locationName.JINJOHP4,
|
|
locationName.JINJOHP3
|
|
])
|
|
prefill_locations_with_item(itemName.BRJINJO, [
|
|
locationName.JINJOGM1,
|
|
locationName.JINJOJR2,
|
|
locationName.JINJOTL2,
|
|
locationName.JINJOTL5
|
|
])
|
|
prefill_locations_with_item(itemName.GJINJO, [
|
|
locationName.JINJOWW5,
|
|
locationName.JINJOJR1,
|
|
locationName.JINJOTL4,
|
|
locationName.JINJOGI2,
|
|
locationName.JINJOHP1
|
|
])
|
|
prefill_locations_with_item(itemName.RJINJO, [
|
|
locationName.JINJOMT2,
|
|
locationName.JINJOMT3,
|
|
locationName.JINJOMT5,
|
|
locationName.JINJOJR3,
|
|
locationName.JINJOJR4,
|
|
locationName.JINJOWW2
|
|
])
|
|
prefill_locations_with_item(itemName.BLJINJO, [
|
|
locationName.JINJOGM3,
|
|
locationName.JINJOTL1,
|
|
locationName.JINJOHP5,
|
|
locationName.JINJOCC2,
|
|
locationName.JINJOIH1,
|
|
locationName.JINJOIH4,
|
|
locationName.JINJOIH5
|
|
])
|
|
prefill_locations_with_item(itemName.PJINJO, [
|
|
locationName.JINJOMT1,
|
|
locationName.JINJOGM5,
|
|
locationName.JINJOCC1,
|
|
locationName.JINJOCC3,
|
|
locationName.JINJOCC5,
|
|
locationName.JINJOIH2,
|
|
locationName.JINJOIH3,
|
|
locationName.JINJOGI4
|
|
])
|
|
prefill_locations_with_item(itemName.BKJINJO, [
|
|
locationName.JINJOMT4,
|
|
locationName.JINJOGM2,
|
|
locationName.JINJOGM4,
|
|
locationName.JINJOWW1,
|
|
locationName.JINJOTL3,
|
|
locationName.JINJOGI1,
|
|
locationName.JINJOGI5,
|
|
locationName.JINJOCC4,
|
|
locationName.JINJOGI3
|
|
])
|
|
|
|
def allow_extra_jiggies_roll(self) -> bool:
|
|
return self.options.replace_extra_jiggies.value and self.jiggies_in_pool < self.hard_item_limit
|
|
|
|
def allow_extra_notes_roll(self) -> bool:
|
|
return self.options.replace_extra_notes.value\
|
|
and self.options.randomize_notes.value\
|
|
and self.notes_in_pool < self.hard_item_limit
|
|
|
|
def allow_extra_doubloons_roll(self) -> bool:
|
|
return self.options.randomize_doubloons.value and self.doubloons_in_pool < self.hard_item_limit
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
trap_weights = [
|
|
(itemName.GNEST, self.options.golden_eggs_weight.value),
|
|
(itemName.TTRAP, self.options.trip_trap_weight.value),
|
|
(itemName.STRAP, self.options.slip_trap_weight.value),
|
|
(itemName.TRTRAP, self.options.transform_trap_weight.value),
|
|
(itemName.SQTRAP, self.options.squish_trap_weight.value),
|
|
(itemName.TITRAP, self.options.tip_trap_weight.value),
|
|
]
|
|
filler_weights = [
|
|
(itemName.JIGGY_AS_USEFUL, self.options.extra_jiggies_weight.value if self.allow_extra_jiggies_roll() else 0),
|
|
(itemName.NOTE_AS_USEFUL, self.options.extra_notes_weight.value if self.allow_extra_notes_roll() else 0),
|
|
(itemName.DOUBLOON_AS_USEFUL, self.options.extra_doubloons_weight
|
|
if self.allow_extra_doubloons_roll() else 0),
|
|
(itemName.ENEST, self.options.egg_nests_weight.value * (2 if self.options.nestsanity.value else 1)),
|
|
(itemName.FNEST, self.options.feather_nests_weight.value * (2 if self.options.nestsanity.value else 1)),
|
|
(itemName.NONE, self.options.big_o_pants_weight.value)
|
|
]
|
|
|
|
if self.traps_in_pool < self.options.max_traps.value:
|
|
weights = trap_weights + filler_weights
|
|
else:
|
|
weights = filler_weights
|
|
|
|
names, actual_weights = zip(*weights)
|
|
|
|
if sum(actual_weights) == 0:
|
|
actual_weights = (*actual_weights[:-1], 1)
|
|
|
|
return self.random.choices(names, actual_weights, k=1)[0]
|
|
|
|
def banjo_pre_fills(self, itemNameOrGroup: str, group: str | None, useGroup: bool) -> None:
|
|
if useGroup:
|
|
for group_name, item_info in self.item_name_groups.items():
|
|
if group_name == itemNameOrGroup:
|
|
for name in item_info:
|
|
item = self.create_item(name)
|
|
banjoItem = all_item_table.get(name)
|
|
# self.multiworld.get_location(banjoItem.defualt_location, self.player).place_locked_item(item)
|
|
location = self.get_location(banjoItem.default_location)
|
|
location.place_locked_item(item)
|
|
else:
|
|
for name, id in self.location_name_to_id.items():
|
|
item = self.create_item(itemNameOrGroup)
|
|
if self.location_name_to_group[name] == group:
|
|
# self.multiworld.get_location(name, self.player).place_locked_item(item)
|
|
location = self.get_location(name)
|
|
location.place_locked_item(item)
|
|
|
|
@classmethod
|
|
def stage_fill_hook(
|
|
cls,
|
|
multiworld: MultiWorld,
|
|
progitempool: list[Item],
|
|
usefulitempool: list[Item],
|
|
filleritempool: list[Item],
|
|
fill_locations: list[Location]
|
|
):
|
|
# If there are a lot of items to fit in few locations, help out the generator by sorting the items into a more
|
|
# easy to fill order. This reduces the chance of the fill algorithm getting stuck and having to swap items.
|
|
# 0.75 is a heuristic threshold; default BT settings is about 82% of the pool being progression.
|
|
if len(progitempool) / len(fill_locations) < 0.75:
|
|
return
|
|
|
|
bt_players = {world.player for world in multiworld.get_game_worlds(cls.game)}
|
|
|
|
# Count how many copies of each item exist for each BT player.
|
|
# Items with more copies are placed first, which helps AP's fill algorithm.
|
|
item_counts: dict[int, Counter[str]] = {player: Counter() for player in bt_players}
|
|
for item in progitempool:
|
|
if item.player in bt_players:
|
|
item_counts[item.player][item.name] += 1
|
|
|
|
def sort_pool(item: Item) -> int:
|
|
if item.player in item_counts:
|
|
return item_counts[item.player][item.name]
|
|
else:
|
|
# If it's not an item belonging to a Banjo-Tooie player, keep its order the same.
|
|
return 0
|
|
|
|
progitempool.sort(key=sort_pool)
|
|
|
|
@classmethod
|
|
def stage_write_spoiler(cls, world, spoiler_handle):
|
|
entrance_hags = {
|
|
regionName.MT: regionName.MTE,
|
|
regionName.GM: regionName.GGME,
|
|
regionName.WW: regionName.WWE,
|
|
regionName.JR: regionName.JRLE,
|
|
regionName.TL: regionName.TDLE,
|
|
regionName.GIO: regionName.GIE,
|
|
regionName.HP: regionName.HFPE,
|
|
regionName.CC: regionName.CCLE,
|
|
regionName.CK: regionName.CKE,
|
|
regionName.MTBOSS: regionName.MTTT,
|
|
regionName.GMBOSS: regionName.CHUFFY,
|
|
regionName.WWBOSS: regionName.WW,
|
|
regionName.JRBOSS: regionName.JRLC,
|
|
regionName.TLBOSS: regionName.TLTOP,
|
|
regionName.GIBOSS: regionName.GI1,
|
|
regionName.HPFBOSS: regionName.HP,
|
|
regionName.HPIBOSS: regionName.HP,
|
|
regionName.CCBOSS: regionName.CC,
|
|
}
|
|
bt_players = world.get_game_players(cls.game)
|
|
spoiler_handle.write(f"\n\nBanjo-Tooie ({cls.world_version.as_simple_string()})")
|
|
for player in bt_players:
|
|
currentWorld: BanjoTooieWorld = world.worlds[player]
|
|
name = world.get_player_name(player)
|
|
spoiler_handle.write(f"\n\n{name}:")
|
|
spoiler_handle.write('\n\tLoading Zones:')
|
|
for starting_zone, actual_world in currentWorld.loading_zones.items():
|
|
if actual_world == regionName.JR:
|
|
spoiler_handle.write(f"\n\t\t{entrance_hags[starting_zone]} -> Jolly Roger's Lagoon")
|
|
elif actual_world == regionName.GIO:
|
|
spoiler_handle.write(f"\n\t\t{entrance_hags[starting_zone]} -> Grunty Industries")
|
|
else:
|
|
spoiler_handle.write(f"\n\t\t{entrance_hags[starting_zone]} -> {actual_world}")
|
|
spoiler_handle.write("\n\tWorld Requirements:")
|
|
for entrances, cost in currentWorld.world_requirements.items():
|
|
if entrances == regionName.JR:
|
|
spoiler_handle.write(f"\n\t\tJolly Roger's Lagoon: {cost}")
|
|
elif entrances == regionName.GIO:
|
|
spoiler_handle.write(f"\n\t\tGrunty Industries: {cost}")
|
|
else:
|
|
spoiler_handle.write(f"\n\t\t{entrances}: {cost}")
|
|
spoiler_handle.write("\n\tJamjars' Silo Costs:")
|
|
for silo, cost in currentWorld.jamjars_siloname_costs.items():
|
|
spoiler_handle.write(f"\n\t\t{silo}: {cost}")
|
|
spoiler_handle.write('\n\tHints:')
|
|
for location_id, hint_data in currentWorld.hints.items():
|
|
spoiler_handle.write("\n\t\t{}: {}".format(
|
|
currentWorld.location_id_to_name[location_id],
|
|
hint_data.text
|
|
))
|
|
|
|
def post_fill(self) -> None:
|
|
if not BanjoTooieWorld.has_converted_progression_items:
|
|
BanjoTooieWorld.has_converted_progression_items = True
|
|
# Now that all the items are placed, we can re-add the progression flag for fillers.
|
|
for location in self.multiworld.get_locations():
|
|
if location.item.game == self.game:
|
|
if location.item.name in [itemName.NOTE, itemName.DOUBLOON, itemName.JIGGY]:
|
|
location.item.classification = ItemClassification.progression
|
|
|
|
def finalize_multiworld(self):
|
|
choose_hinted_locations(self)
|
|
|
|
def pre_output(self):
|
|
generate_hint_data(self)
|
|
|
|
def fill_slot_data(self) -> Dict[str, Any]:
|
|
btoptions = {option_name: option.value for option_name, option in self.options.__dict__.items()}
|
|
|
|
# plando_items not serialisable, so we can't include it in slot_data.
|
|
btoptions.pop("plando_items")
|
|
|
|
# Elements that are randomised outside the yaml and affects gameplay
|
|
custom_bt_data: Dict[str, Any] = {
|
|
"player_name": self.player_name,
|
|
"seed": self.random.randint(12212, 9090763),
|
|
"world_order": self.world_order,
|
|
"world_requirements": self.world_requirements,
|
|
"loading_zones": self.loading_zones,
|
|
"preopened_silos_names": self.preopened_silos,
|
|
"preopened_silos_ids": [self.item_name_to_id[name] for name in self.preopened_silos],
|
|
"version": f"{self.world_version.as_simple_string()}",
|
|
"jamjars_siloname_costs": self.jamjars_siloname_costs,
|
|
"jamjars_silo_costs": self.jamjars_silo_costs,
|
|
"hints": {location: asdict(hint_data) for location, hint_data in self.hints.items()}
|
|
}
|
|
slot_data = {
|
|
"options": btoptions,
|
|
"custom_bt_data": custom_bt_data,
|
|
}
|
|
return slot_data
|
|
|
|
# for the universal tracker, doesn't get called in standard gen
|
|
def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
|
return slot_data
|
|
|
|
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
|
# For hints, we choose to hint the level for which the collectible would count.
|
|
# For example, Dippy Jiggy would hint to TDL.
|
|
|
|
# For items in boss rooms, we hint the level that leads to the boss room, if boss rooms are randomised.
|
|
# This has priority over the level entrance.
|
|
|
|
def add_level_loading_zone_information(
|
|
hint_information: Dict[int, str],
|
|
locations: Dict[str, LocationData],
|
|
level: str
|
|
):
|
|
entrance_lookup = {
|
|
regionName.MT: regionName.MTE,
|
|
regionName.GM: regionName.GGME,
|
|
regionName.WW: regionName.WWE,
|
|
regionName.JR: regionName.JRLE,
|
|
regionName.TL: regionName.TDLE,
|
|
regionName.GIO: regionName.GIE,
|
|
regionName.HP: regionName.HFPE,
|
|
regionName.CC: regionName.CCLE,
|
|
regionName.CK: regionName.CKE,
|
|
}
|
|
for data in locations.values():
|
|
entrance_to_level = list(self.loading_zones.keys())[list(self.loading_zones.values()).index(level)]
|
|
hint_information.update({data.btid: entrance_lookup[entrance_to_level]})
|
|
|
|
hints = {}
|
|
if self.options.randomize_world_entrance_loading_zones.value:
|
|
add_level_loading_zone_information(hints, MTLoc_Table, regionName.MT)
|
|
add_level_loading_zone_information(hints, GMLoc_table, regionName.GM)
|
|
add_level_loading_zone_information(hints, WWLoc_table, regionName.WW)
|
|
add_level_loading_zone_information(hints, JRLoc_table, regionName.JR)
|
|
add_level_loading_zone_information(hints, TLLoc_table, regionName.TL)
|
|
add_level_loading_zone_information(hints, GILoc_table, regionName.GIO)
|
|
add_level_loading_zone_information(hints, HPLoc_table, regionName.HP)
|
|
add_level_loading_zone_information(hints, CCLoc_table, regionName.CC)
|
|
|
|
if self.options.randomize_boss_loading_zones.value:
|
|
boss_entrance_lookup = {
|
|
regionName.MTBOSS: regionName.MTTT,
|
|
regionName.GMBOSS: regionName.CHUFFY,
|
|
regionName.WWBOSS: regionName.WW,
|
|
regionName.JRBOSS: regionName.JRLC,
|
|
regionName.TLBOSS: regionName.TLTOP,
|
|
regionName.GIBOSS: regionName.GI1,
|
|
regionName.HPFBOSS: regionName.HP,
|
|
regionName.HPIBOSS: regionName.HP,
|
|
regionName.CCBOSS: regionName.CC,
|
|
}
|
|
for boss_region_name in boss_entrance_lookup.keys():
|
|
for location in self.get_region(boss_region_name).locations:
|
|
entrance_name = list(self.loading_zones.keys())[
|
|
list(self.loading_zones.values()).index(boss_region_name)
|
|
]
|
|
hints.update({location.address: boss_entrance_lookup[entrance_name]})
|
|
|
|
hint_data.update({self.player: hints})
|