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
1303 lines
69 KiB
Python
1303 lines
69 KiB
Python
"""Spoiler class and functions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from copy import deepcopy
|
|
from typing import TYPE_CHECKING, Dict, List, Optional, OrderedDict, Union
|
|
|
|
import randomizer.Lists.Exceptions as Ex
|
|
from randomizer.Enums.Events import Events
|
|
from randomizer.Enums.Items import Items
|
|
from randomizer.Enums.Kongs import Kongs
|
|
from randomizer.Enums.Levels import Levels
|
|
from randomizer.Enums.Locations import Locations
|
|
from randomizer.Enums.Maps import Maps
|
|
from randomizer.Enums.MoveTypes import MoveTypes
|
|
from randomizer.Enums.Regions import Regions
|
|
from randomizer.Enums.HintRegion import HintRegion
|
|
from randomizer.Enums.SwitchTypes import SwitchType
|
|
from randomizer.Enums.Settings import (
|
|
BananaportRando,
|
|
CBRando,
|
|
DKPortalRando,
|
|
GlitchesSelected,
|
|
LogicType,
|
|
HardBossesSelected,
|
|
MinigameBarrels,
|
|
ProgressiveHintItem,
|
|
RandomPrices,
|
|
ShockwaveStatus,
|
|
ShuffleLoadingZones,
|
|
ShufflePortLocations,
|
|
SpoilerHints,
|
|
TrainingBarrels,
|
|
WinConditionComplex,
|
|
)
|
|
from randomizer.Enums.Transitions import Transitions
|
|
from randomizer.Enums.Types import Types, BarrierItems
|
|
from randomizer.Lists.EnemyTypes import EnemyMetaData
|
|
from randomizer.Lists.Item import ItemFromKong, ItemList, KongFromItem, NameFromKong
|
|
from randomizer.Lists.Location import LocationListOriginal, PreGivenLocations, TrainingBarrelLocations
|
|
from randomizer.Lists.Logic import GlitchLogicItems
|
|
from randomizer.Enums.Maps import Maps
|
|
from randomizer.Lists.MapsAndExits import GetExitId, GetMapId
|
|
from randomizer.Lists.Minigame import (
|
|
BarrelMetaData,
|
|
HelmMinigameLocations,
|
|
MinigameRequirements,
|
|
TrainingMinigameLocations,
|
|
MinigameSelector,
|
|
)
|
|
from randomizer.Lists.Multiselectors import FasterCheckSelector, RemovedBarrierSelector, QoLSelector
|
|
from randomizer.Lists.EnemyTypes import EnemySelector
|
|
from randomizer.Logic import CollectibleRegionsOriginal, LogicVarHolder, RegionsOriginal
|
|
from randomizer.Prices import ProgressiveMoves
|
|
from randomizer.Settings import Settings
|
|
from randomizer.ShuffleBosses import HardBossesEnabled
|
|
from randomizer.ShuffleExits import ShufflableExits
|
|
from randomizer.ShuffleKasplats import constants, shufflable
|
|
from randomizer.Patching.Library.Generic import IsItemSelected
|
|
|
|
if TYPE_CHECKING:
|
|
from randomizer.Lists.Location import Location
|
|
from randomizer.LogicClasses import Sphere
|
|
|
|
boss_map_names = {
|
|
Maps.JapesBoss: "Army Dillo 1",
|
|
Maps.AztecBoss: "Dogadon 1",
|
|
Maps.FactoryBoss: "Mad Jack",
|
|
Maps.GalleonBoss: "Pufftoss",
|
|
Maps.FungiBoss: "Dogadon 2",
|
|
Maps.CavesBoss: "Army Dillo 2",
|
|
Maps.CastleBoss: "King Kut Out",
|
|
Maps.KroolDonkeyPhase: "DK Phase",
|
|
Maps.KroolDiddyPhase: "Diddy Phase",
|
|
Maps.KroolLankyPhase: "Lanky Phase",
|
|
Maps.KroolTinyPhase: "Tiny Phase",
|
|
Maps.KroolChunkyPhase: "Chunky Phase",
|
|
}
|
|
|
|
|
|
class Spoiler:
|
|
"""Class which contains all spoiler data passed into and out of randomizer."""
|
|
|
|
def __init__(self, settings: Settings) -> None:
|
|
"""Initialize spoiler just with settings."""
|
|
self.settings: Settings = settings
|
|
self.playthrough = {}
|
|
self.woth = {}
|
|
self.woth_locations = {}
|
|
self.woth_paths = {}
|
|
self.krool_paths = {}
|
|
self.rap_win_con_paths = {}
|
|
self.other_paths = {}
|
|
self.shuffled_door_data = {}
|
|
self.shuffled_barrel_data = {}
|
|
self.shuffled_exit_data = {}
|
|
self.shuffled_exit_instructions = []
|
|
self.music_bgm_data = {}
|
|
self.music_majoritem_data = {}
|
|
self.music_minoritem_data = {}
|
|
self.music_event_data = {}
|
|
self.location_data = {}
|
|
self.enemy_replacements = []
|
|
self.cb_placements = []
|
|
self.LogicVariables = LogicVarHolder(self)
|
|
self.RegionList = deepcopy(RegionsOriginal)
|
|
self.CollectibleRegions = deepcopy(CollectibleRegionsOriginal)
|
|
self.LocationList = deepcopy(LocationListOriginal)
|
|
|
|
self.move_data = []
|
|
# 0: Cranky, 1: Funky, 2: Candy
|
|
for move_master_type in range(3):
|
|
master_moves = []
|
|
if move_master_type == 0:
|
|
# Shop
|
|
for shop_index in range(3):
|
|
moves = []
|
|
# One for each kong
|
|
for kong_index in range(5):
|
|
kongmoves = []
|
|
# One for each level
|
|
for level_index in range(8):
|
|
kongmoves.append({"move_type": None})
|
|
moves.append(kongmoves)
|
|
master_moves.append(moves)
|
|
elif move_master_type == 1:
|
|
# Training Barrels
|
|
if self.settings.training_barrels == TrainingBarrels.normal:
|
|
for tbarrel_type in ["dive", "orange", "barrel", "vine"]:
|
|
master_moves.append({"move_type": "flag", "flag": tbarrel_type, "price": 0})
|
|
else:
|
|
for tbarrel_type in ["dive", "orange", "barrel", "vine"]:
|
|
master_moves.append({"move_type": None})
|
|
elif move_master_type == 2:
|
|
# BFI
|
|
if self.settings.shockwave_status == ShockwaveStatus.vanilla:
|
|
master_moves = [{"move_type": "flag", "flag": "camera_shockwave", "price": 0}]
|
|
else:
|
|
master_moves = [{"move_type": None}]
|
|
self.move_data.append(master_moves)
|
|
|
|
self.hint_list = {}
|
|
self.short_hint_list = {}
|
|
self.tied_hint_flags = {}
|
|
self.tied_hint_regions = [HintRegion.NoRegion] * 35
|
|
self.settings.finalize_world_settings(self)
|
|
self.settings.update_valid_locations(self)
|
|
|
|
def FlushAllExcessSpoilerData(self):
|
|
"""Flush all spoiler data that is not needed for the final result."""
|
|
del self.LocationList
|
|
del self.RegionList
|
|
del self.CollectibleRegions
|
|
del self.LogicVariables
|
|
|
|
def Reset(self) -> None:
|
|
"""Reset logic variables and region info that should be reset before a search."""
|
|
self.LogicVariables.Reset()
|
|
self.ResetRegionAccess()
|
|
self.ResetCollectibleRegions()
|
|
|
|
def ResetRegionAccess(self) -> None:
|
|
"""Reset kong access for all regions."""
|
|
for region in self.RegionList.values():
|
|
region.ResetAccess()
|
|
|
|
def ResetCollectibleRegions(self) -> None:
|
|
"""Reset if each collectible has been added."""
|
|
for region in self.CollectibleRegions.values():
|
|
for collectible in region:
|
|
collectible.added = False
|
|
# collectible.enabled = collectible.vanilla
|
|
|
|
def ClearAllLocations(self) -> None:
|
|
"""Clear item from every location."""
|
|
for location in self.LocationList.values():
|
|
location.item = None
|
|
# Always block PreGiven locations and only unblock them as we intentionally place moves there
|
|
for location_id in TrainingBarrelLocations:
|
|
self.LocationList[location_id].inaccessible = True
|
|
for location_id in PreGivenLocations:
|
|
self.LocationList[location_id].inaccessible = True
|
|
|
|
def ResetLocationList(self) -> None:
|
|
"""Reset the LocationList to values conducive to a new fill."""
|
|
for location in self.LocationList.values():
|
|
location.PlaceDefaultItem(self)
|
|
# Known to be incomplete - it should also confirm the correct locations of Fairies, Dirt, and Crowns
|
|
|
|
def InitKasplatMap(self) -> None:
|
|
"""Initialize kasplat_map in logic variables with default values."""
|
|
# Just use default kasplat associations.
|
|
self.LogicVariables.kasplat_map = {}
|
|
self.LogicVariables.kasplat_map.update(shufflable)
|
|
self.LogicVariables.kasplat_map.update(constants)
|
|
|
|
def getItemGroup(self, item: Optional[Items]) -> str:
|
|
"""Get item group from item."""
|
|
if item is None:
|
|
item = Items.NoItem
|
|
if item == Items.NoItem:
|
|
return "Empty"
|
|
item_type = ItemList[item].type
|
|
type_dict = {
|
|
Types.Kong: "Kongs",
|
|
Types.Shop: "Moves",
|
|
Types.Shockwave: "Moves",
|
|
Types.TrainingBarrel: "Moves",
|
|
Types.Climbing: "Moves",
|
|
Types.Banana: "Golden Bananas",
|
|
Types.Blueprint: "Blueprints",
|
|
Types.Fairy: "Fairies",
|
|
Types.Key: "Keys",
|
|
Types.Crown: "Crowns",
|
|
Types.Medal: "Medals",
|
|
Types.NintendoCoin: "Company Coins",
|
|
Types.RarewareCoin: "Company Coins",
|
|
Types.Bean: "Miscellaneous Items",
|
|
Types.Pearl: "Miscellaneous Items",
|
|
Types.RainbowCoin: "Rainbow Coins",
|
|
Types.FakeItem: "Ice Traps",
|
|
Types.JunkItem: "Junk Items",
|
|
Types.CrateItem: "Melon Crates",
|
|
Types.Enemies: "Enemy Drops",
|
|
Types.Cranky: "Shop Owners",
|
|
Types.Funky: "Shop Owners",
|
|
Types.Candy: "Shop Owners",
|
|
Types.Snide: "Shop Owners",
|
|
Types.Hint: "Hints",
|
|
}
|
|
if item_type in type_dict:
|
|
return type_dict[item_type]
|
|
return "Unknown"
|
|
|
|
def dumpMultiselector(self, toggle: bool, settings_list: list, selector_list: list):
|
|
"""Dump multiselector list to a response which can be dumped to the spoiler."""
|
|
if toggle and any(settings_list):
|
|
lst = []
|
|
selector_name_dict = {}
|
|
for x in selector_list:
|
|
selector_name_dict[x["value"]] = x["name"]
|
|
for x in settings_list:
|
|
if x.name in selector_name_dict:
|
|
lst.append(selector_name_dict[x.name])
|
|
return lst
|
|
return toggle
|
|
|
|
def createJson(self) -> None:
|
|
"""Convert spoiler to JSON and save it."""
|
|
# We want to convert raw spoiler data into the important bits and in human-readable formats.
|
|
humanspoiler = OrderedDict()
|
|
# Settings data
|
|
settings = OrderedDict()
|
|
settings["Settings String"] = self.settings.settings_string
|
|
settings["Seed"] = self.settings.seed_id
|
|
# settings["algorithm"] = self.settings.algorithm # Don't need this for now, probably
|
|
logic_types = {
|
|
LogicType.nologic: "No Logic",
|
|
LogicType.glitch: "Glitched Logic",
|
|
LogicType.glitchless: "Glitchless Logic",
|
|
}
|
|
if self.settings.logic_type in logic_types:
|
|
settings["Logic Type"] = logic_types[self.settings.logic_type]
|
|
else:
|
|
settings["Logic Type"] = self.settings.logic_type
|
|
if self.settings.logic_type == LogicType.glitch:
|
|
settings["Glitches Enabled"] = ", ".join(
|
|
[x.name for x in GlitchLogicItems if GlitchesSelected[x.shorthand] in self.settings.glitches_selected or len(self.settings.glitches_selected) == 0]
|
|
)
|
|
settings["Shuffle Enemies"] = self.settings.enemy_rando
|
|
settings["Move Randomization type"] = self.settings.move_rando.name
|
|
settings["Loading Zones Shuffled"] = self.settings.shuffle_loading_zones.name
|
|
settings["Decoupled Loading Zones"] = self.settings.decoupled_loading_zones
|
|
settings["Helm Location Shuffled"] = self.settings.shuffle_helm_location
|
|
startKongList = []
|
|
for x in self.settings.starting_kong_list:
|
|
startKongList.append(x.name.capitalize())
|
|
settings["Hard B Lockers"] = self.settings.hard_blockers
|
|
if self.settings.randomize_blocker_required_amounts:
|
|
settings["Maximum B Locker"] = self.settings.blocker_text
|
|
if self.settings.maximize_helm_blocker:
|
|
settings["Maximum B Locker ensured"] = self.settings.maximize_helm_blocker
|
|
settings["Hard Troff N Scoff"] = self.settings.hard_troff_n_scoff
|
|
if self.settings.randomize_cb_required_amounts:
|
|
settings["Maximum Troff N Scoff"] = self.settings.troff_text
|
|
settings["Open Lobbies"] = self.settings.open_lobbies
|
|
settings["Auto Complete Bonus Barrels"] = self.settings.bonus_barrel_auto_complete
|
|
settings["Auto Key Turn ins"] = self.settings.auto_keys
|
|
settings["Chaos B.Lockers"] = self.settings.chaos_blockers
|
|
settings["Chaos Ratio"] = self.settings.chaos_ratio
|
|
settings["Complex Level Order"] = self.settings.hard_level_progression
|
|
settings["Progressive Switch Strength"] = self.settings.alter_switch_allocation
|
|
settings["Hard Shooting"] = self.settings.hard_shooting
|
|
settings["Dropsanity"] = self.settings.enemy_drop_rando
|
|
settings["Switchsanity"] = self.settings.switchsanity.name
|
|
settings["Free Trade Agreement"] = self.settings.free_trade_setting.name
|
|
settings["Randomize Pickups"] = self.settings.randomize_pickups
|
|
settings["Randomize Patches"] = self.settings.random_patches
|
|
settings["Randomize Crates"] = self.settings.random_crates
|
|
settings["Randomize CB Locations"] = self.settings.cb_rando_enabled
|
|
settings["Randomize Coin Locations"] = self.settings.coin_rando
|
|
settings["Randomize Shop Locations"] = self.settings.shuffle_shops
|
|
settings["Randomize Kasplats"] = self.settings.kasplat_rando_setting.name
|
|
settings["Randomize Banana Fairies"] = self.settings.random_fairies
|
|
settings["Randomize Battle Arenas"] = self.settings.crown_placement_rando
|
|
settings["Vanilla Door Shuffle"] = self.settings.vanilla_door_rando
|
|
settings["Dos' Doors"] = self.settings.dos_door_rando
|
|
settings["Randomize Wrinkly Doors"] = self.settings.wrinkly_location_rando
|
|
settings["Randomize T&S Portals"] = self.settings.tns_location_rando
|
|
settings["Puzzle Randomization"] = self.settings.puzzle_rando_difficulty.name
|
|
settings["Crown Door Open"] = self.settings.crown_door_item == BarrierItems.Nothing
|
|
settings["Coin Door Open"] = self.settings.coin_door_item == BarrierItems.Nothing
|
|
settings["Shockwave Shuffle"] = self.settings.shockwave_status.name
|
|
settings["Random Jetpac Medal Requirement"] = self.settings.random_medal_requirement
|
|
settings["Bananas Required for Medal"] = self.settings.medal_cb_req
|
|
settings["Fairies Required for Rareware GB"] = self.settings.rareware_gb_fairies
|
|
settings["Pearls Required for Mermaid GB"] = self.settings.mermaid_gb_pearls
|
|
settings["Random Shop Prices"] = self.settings.random_prices.name
|
|
settings["Banana Port Randomization"] = self.settings.bananaport_rando.name
|
|
settings["Banana port Location Shuffle"] = self.settings.bananaport_placement_rando.name
|
|
settings["Activated Warps"] = self.settings.activate_all_bananaports.name
|
|
settings["Smaller Shops"] = self.settings.smaller_shops
|
|
settings["Irondonk"] = self.settings.perma_death
|
|
settings["Disable Tag Barrels"] = self.settings.disable_tag_barrels
|
|
settings["Ice Trap Frequency"] = self.settings.ice_trap_frequency.name
|
|
settings["Ice Traps Damage Player"] = self.settings.ice_traps_damage
|
|
settings["Mirror Mode"] = self.settings.mirror_mode
|
|
settings["Damage Amount"] = self.settings.damage_amount.name
|
|
settings["Hard Mode Enabled"] = self.settings.hard_mode and len(self.settings.hard_mode_selected) > 0
|
|
settings["Hard Bosses Enabled"] = self.settings.hard_bosses and len(self.settings.hard_bosses_selected) > 0
|
|
# settings["Krusha Slot"] = self.settings.krusha_ui.name
|
|
settings["DK Model"] = self.settings.kong_model_dk.name
|
|
settings["Diddy Model"] = self.settings.kong_model_diddy.name
|
|
settings["Lanky Model"] = self.settings.kong_model_lanky.name
|
|
settings["Tiny Model"] = self.settings.kong_model_tiny.name
|
|
settings["Chunky Model"] = self.settings.kong_model_chunky.name
|
|
|
|
settings["Key 8 Required"] = self.settings.krool_access
|
|
settings["Vanilla K. Rool Requirement"] = self.settings.k_rool_vanilla_requirement
|
|
settings["Key 8 in Helm"] = self.settings.key_8_helm
|
|
settings["Select Starting Keys"] = self.settings.select_keys
|
|
if not self.settings.keys_random:
|
|
settings["Number of Keys Required"] = self.settings.krool_key_count
|
|
settings["Starting Moves Count"] = self.settings.starting_moves_count
|
|
settings["Fast Start"] = self.settings.fast_start_beginning_of_game
|
|
settings["Helm Setting"] = self.settings.helm_setting.name
|
|
settings["Helm Room Bonus Count"] = int(self.settings.helm_room_bonus_count)
|
|
settings["Tag Anywhere"] = self.settings.enable_tag_anywhere
|
|
settings["Kongless Hint Doors"] = self.settings.wrinkly_available
|
|
settings["Quality of Life"] = self.dumpMultiselector(self.settings.quality_of_life, self.settings.misc_changes_selected, QoLSelector)
|
|
settings["Fast GBs"] = self.dumpMultiselector(self.settings.faster_checks_enabled, self.settings.faster_checks_selected, FasterCheckSelector)
|
|
settings["Barriers Removed"] = self.dumpMultiselector(self.settings.remove_barriers_enabled, self.settings.remove_barriers_selected, RemovedBarrierSelector)
|
|
settings["Random Win Condition"] = self.settings.win_condition_random
|
|
if not self.settings.win_condition_random:
|
|
wc_count = self.settings.win_condition_count
|
|
win_con_name_table = {
|
|
WinConditionComplex.beat_krool: "Beat K. Rool",
|
|
WinConditionComplex.get_key8: "Acquire Key 8",
|
|
WinConditionComplex.krem_kapture: "Kremling Kapture",
|
|
WinConditionComplex.dk_rap_items: "Complete the Rap",
|
|
WinConditionComplex.req_bean: "Acquire the Bean",
|
|
WinConditionComplex.req_bp: f"{wc_count} Blueprint{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_companycoins: f"{wc_count} Company Coin{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_crown: f"{wc_count} Crown{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_fairy: f"{wc_count} Fair{'ies' if wc_count != 1 else 'y'}",
|
|
WinConditionComplex.req_gb: f"{wc_count} Golden Banana{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_key: f"{wc_count} Key{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_medal: f"{wc_count} Medal{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_pearl: f"{wc_count} Pearl{'s' if wc_count != 1 else ''}",
|
|
WinConditionComplex.req_rainbowcoin: f"{wc_count} Rainbow Coin{'s' if wc_count != 1 else ''}",
|
|
}
|
|
if self.settings.win_condition_item in win_con_name_table:
|
|
settings["Win Condition"] = win_con_name_table[self.settings.win_condition_item]
|
|
else:
|
|
settings["Win Condition"] = self.settings.win_condition_item.name
|
|
settings["Fungi Time of Day"] = self.settings.fungi_time.name
|
|
settings["Galleon Water Level"] = self.settings.galleon_water.name
|
|
settings["Chunky Phase Slam Requirement"] = self.settings.chunky_phase_slam_req.name
|
|
settings["Hint Preset"] = self.settings.wrinkly_hints
|
|
if self.settings.progressive_hint_item != ProgressiveHintItem.off:
|
|
settings["Progressive Hint Item"] = self.settings.progressive_hint_item.name
|
|
settings["Progressive Hint Cap"] = int(self.settings.progressive_hint_count)
|
|
settings["Dim Solved Hints"] = self.settings.dim_solved_hints
|
|
settings["No Joke Hints"] = self.settings.serious_hints
|
|
settings["Item Reward Previews"] = self.settings.item_reward_previews
|
|
settings["Bonus Barrel Rando"] = self.dumpMultiselector(self.settings.bonus_barrel_rando, self.settings.minigames_list_selected, MinigameSelector)
|
|
if self.settings.enemy_rando and any(self.settings.enemies_selected):
|
|
value_lst = [x.name for x in self.settings.enemies_selected]
|
|
settings["Enemy Rando"] = [enemy["name"] for enemy in EnemySelector if enemy["value"] in value_lst]
|
|
else:
|
|
settings["Enemy Rando"] = self.settings.enemy_rando
|
|
settings["Crown Enemy Rando"] = self.settings.crown_enemy_difficulty.name
|
|
if self.settings.helm_hurry:
|
|
settings["Game Mode"] = "Helm Hurry"
|
|
humanspoiler["Settings"] = settings
|
|
humanspoiler["Randomizer Version"] = self.settings.version
|
|
humanspoiler["Generation Branch"] = self.settings.branch
|
|
humanspoiler["Cosmetics"] = {}
|
|
if self.settings.spoiler_hints != SpoilerHints.off:
|
|
humanspoiler["Spoiler Hints Data"] = {}
|
|
for key in self.level_spoiler.keys():
|
|
if key == "point_spread":
|
|
humanspoiler["Spoiler Hints Data"][key] = json.dumps(self.level_spoiler[key])
|
|
else:
|
|
humanspoiler["Spoiler Hints Data"][key] = self.level_spoiler[key].toJSON()
|
|
humanspoiler["Spoiler Hints"] = self.level_spoiler_human_readable
|
|
humanspoiler["Requirements"] = {}
|
|
if self.settings.random_starting_region:
|
|
humanspoiler["Game Start"] = {}
|
|
humanspoiler["Game Start"]["Starting Kong List"] = startKongList
|
|
humanspoiler["Game Start"]["Starting Region"] = self.settings.starting_region["region_name"]
|
|
humanspoiler["Game Start"]["Starting Exit"] = self.settings.starting_region["exit_name"]
|
|
# GB Counts
|
|
gb_counts = {}
|
|
level_list = [
|
|
"Jungle Japes",
|
|
"Angry Aztec",
|
|
"Frantic Factory",
|
|
"Gloomy Galleon",
|
|
"Fungi Forest",
|
|
"Crystal Caves",
|
|
"Creepy Castle",
|
|
"Hideout Helm",
|
|
]
|
|
for level_index, amount in enumerate(self.settings.BLockerEntryCount):
|
|
item = self.settings.BLockerEntryItems[level_index].name
|
|
item_total = f" {item}s"
|
|
if item == "Percentage":
|
|
item_total = "%"
|
|
elif item == "Fairy" and amount != 1:
|
|
item_total = " Fairies" # LOL @ English Language
|
|
elif amount == 1:
|
|
item_total = f" {item}"
|
|
gb_counts[level_list[level_index]] = f"{amount}{item_total}"
|
|
humanspoiler["Requirements"]["B Locker Items"] = gb_counts
|
|
# CB Counts
|
|
cb_counts = {}
|
|
for level_index, amount in enumerate(self.settings.BossBananas):
|
|
cb_counts[level_list[level_index]] = amount
|
|
humanspoiler["Requirements"]["Troff N Scoff Bananas"] = cb_counts
|
|
humanspoiler["Requirements"]["Miscellaneous"] = {}
|
|
humanspoiler["Kongs"] = {}
|
|
humanspoiler["Kongs"]["Starting Kong List"] = startKongList
|
|
humanspoiler["Kongs"]["Japes Kong Puzzle Solver"] = ItemList[ItemFromKong(self.settings.diddy_freeing_kong)].name
|
|
humanspoiler["Kongs"]["Tiny Temple Puzzle Solver"] = ItemList[ItemFromKong(self.settings.tiny_freeing_kong)].name
|
|
humanspoiler["Kongs"]["Llama Temple Puzzle Solver"] = ItemList[ItemFromKong(self.settings.lanky_freeing_kong)].name
|
|
humanspoiler["Kongs"]["Factory Kong Puzzle Solver"] = ItemList[ItemFromKong(self.settings.chunky_freeing_kong)].name
|
|
humanspoiler["Requirements"]["Miscellaneous"]["Jetpac Medal Requirement"] = self.settings.medal_requirement
|
|
humanspoiler["End Game"] = {
|
|
"Helm": {},
|
|
"K. Rool": {},
|
|
}
|
|
humanspoiler["End Game"]["K. Rool"]["Keys Required for K Rool"] = self.GetKroolKeysRequired(self.settings.krool_keys_required)
|
|
krool_order = []
|
|
for phase in self.settings.krool_order:
|
|
krool_order.append(boss_map_names[phase])
|
|
humanspoiler["End Game"]["K. Rool"]["K Rool Phases"] = krool_order
|
|
humanspoiler["End Game"]["K. Rool"]["Chunky Phase Slam Requirement"] = self.settings.chunky_phase_slam_req_internal.name
|
|
humanspoiler["End Game"]["K. Rool"]["DK Phase requires Baboon Blast"] = self.settings.cannons_require_blast
|
|
|
|
helm_default_order = [Kongs.donkey, Kongs.chunky, Kongs.tiny, Kongs.lanky, Kongs.diddy]
|
|
helm_new_order = []
|
|
for room in self.settings.helm_order:
|
|
helm_new_order.append(helm_default_order[room].name.capitalize())
|
|
humanspoiler["End Game"]["Helm"]["Helm Rooms"] = helm_new_order
|
|
helm_door_names = {
|
|
BarrierItems.Bean: "Bean",
|
|
BarrierItems.Blueprint: "Blueprints",
|
|
BarrierItems.CompanyCoin: "Company Coins",
|
|
BarrierItems.Crown: "Crowns",
|
|
BarrierItems.Fairy: "Fairies",
|
|
BarrierItems.GoldenBanana: "Golden Bananas",
|
|
BarrierItems.Key: "Keys",
|
|
BarrierItems.Medal: "Medals",
|
|
BarrierItems.Pearl: "Pearls",
|
|
BarrierItems.RainbowCoin: "Rainbow Coins",
|
|
}
|
|
if self.settings.crown_door_item != BarrierItems.Nothing:
|
|
item = self.settings.crown_door_item
|
|
humanspoiler["End Game"]["Helm"]["Crown Door Item"] = helm_door_names[item]
|
|
humanspoiler["End Game"]["Helm"]["Crown Door Item Randomized"] = self.settings.crown_door_random
|
|
humanspoiler["End Game"]["Helm"]["Crown Door Item Amount"] = self.settings.crown_door_item_count
|
|
if self.settings.coin_door_item != BarrierItems.Nothing:
|
|
item = self.settings.coin_door_item
|
|
humanspoiler["End Game"]["Helm"]["Coin Door Item"] = helm_door_names[item]
|
|
humanspoiler["End Game"]["Helm"]["Coin Door Item Randomized"] = self.settings.coin_door_random
|
|
humanspoiler["End Game"]["Helm"]["Coin Door Item Amount"] = self.settings.coin_door_item_count
|
|
if self.settings.shuffle_items:
|
|
humanspoiler["Item Pool"] = list(set([enum.name for enum in self.settings.shuffled_location_types]))
|
|
if self.settings.hard_mode_selected and len(self.settings.hard_mode_selected) > 0:
|
|
humanspoiler["Hard Mode"] = list(set([enum.name for enum in self.settings.hard_mode_selected]))
|
|
if self.settings.hard_bosses_selected and len(self.settings.hard_bosses_selected) > 0:
|
|
humanspoiler["Hard Bosses"] = list(set([enum.name for enum in self.settings.hard_bosses_selected]))
|
|
humanspoiler["Items"] = {
|
|
"Kongs": {},
|
|
"Shops": {},
|
|
"DK Isles": {},
|
|
"Jungle Japes": {},
|
|
"Angry Aztec": {},
|
|
"Frantic Factory": {},
|
|
"Gloomy Galleon": {},
|
|
"Fungi Forest": {},
|
|
"Crystal Caves": {},
|
|
"Creepy Castle": {},
|
|
"Hideout Helm": {},
|
|
"Special": {},
|
|
}
|
|
sorted_item_name = "Items (Sorted by Item)"
|
|
humanspoiler[sorted_item_name] = {
|
|
"Kongs": {},
|
|
"Moves": {},
|
|
"Golden Bananas": {},
|
|
"Blueprints": {},
|
|
"Fairies": {},
|
|
"Keys": {},
|
|
"Crowns": {},
|
|
"Company Coins": {},
|
|
"Medals": {},
|
|
"Miscellaneous Items": {},
|
|
"Rainbow Coins": {},
|
|
"Ice Traps": {},
|
|
"Junk Items": {},
|
|
"Melon Crates": {},
|
|
"Hints": {},
|
|
"Enemy Drops": {},
|
|
"Shop Owners": {},
|
|
"Empty": {},
|
|
"Unknown": {},
|
|
}
|
|
|
|
self.pregiven_items = []
|
|
self.first_move_item = None
|
|
for location_id, location in self.LocationList.items():
|
|
# No need to spoiler constants
|
|
if location.type == Types.Constant or location.inaccessible:
|
|
continue
|
|
# No hints if hint doors are not in the pool
|
|
if location.type == Types.Hint and Types.Hint not in self.settings.shuffled_location_types:
|
|
continue
|
|
if location.type == Types.ProgressiveHint:
|
|
continue
|
|
if location_id in PreGivenLocations:
|
|
if self.settings.fast_start_beginning_of_game or location_id != Locations.IslesFirstMove:
|
|
self.pregiven_items.append(location.item)
|
|
else:
|
|
self.first_move_item = location.item
|
|
# Prevent weird null issues but get the item at the location
|
|
if location.item is None:
|
|
item = Items.NoItem
|
|
else:
|
|
item = ItemList[location.item]
|
|
# Empty PreGiven locations don't really exist and shouldn't show up in the spoiler log
|
|
if location.type in (
|
|
Types.PreGivenMove,
|
|
Types.Cranky,
|
|
Types.Candy,
|
|
Types.Funky,
|
|
Types.Snide,
|
|
) and location.item in (None, Items.NoItem):
|
|
continue
|
|
# Separate Kong locations
|
|
if location.type == Types.Kong:
|
|
humanspoiler["Items"]["Kongs"][location.name] = item.name
|
|
humanspoiler[sorted_item_name][self.getItemGroup(location.item)][location.name] = item.name
|
|
# Separate Shop locations
|
|
elif location.type == Types.Shop:
|
|
# Ignore shop locations with no items
|
|
if location.item is None or location.item == Items.NoItem:
|
|
continue
|
|
# Gotta dig up the price - progressive moves look a little weird in the spoiler
|
|
price = ""
|
|
if location.item in ProgressiveMoves.keys():
|
|
if location.item == Items.ProgressiveSlam:
|
|
price = f"{self.settings.prices[Items.ProgressiveSlam][0]}->{self.settings.prices[Items.ProgressiveSlam][1]}"
|
|
elif location.item == Items.ProgressiveAmmoBelt:
|
|
price = f"{self.settings.prices[Items.ProgressiveAmmoBelt][0]}->{self.settings.prices[Items.ProgressiveAmmoBelt][1]}"
|
|
elif location.item == Items.ProgressiveInstrumentUpgrade:
|
|
price = f"{self.settings.prices[Items.ProgressiveInstrumentUpgrade][0]}->{self.settings.prices[Items.ProgressiveInstrumentUpgrade][1]}->{self.settings.prices[Items.ProgressiveInstrumentUpgrade][2]}"
|
|
# Vanilla prices are by item, not by location
|
|
elif self.settings.random_prices == RandomPrices.vanilla:
|
|
price = str(self.settings.prices[location.item])
|
|
else:
|
|
price = str(self.settings.prices[location_id])
|
|
humanspoiler["Items"]["Shops"][location.name] = item.name + f" ({price})"
|
|
humanspoiler[sorted_item_name][self.getItemGroup(location.item)][location.name] = item.name
|
|
# Filter everything else by level - each location conveniently contains a level-identifying bit in their name
|
|
else:
|
|
level = "Special"
|
|
if "Isles" in location.name or location.type in (
|
|
Types.PreGivenMove,
|
|
Types.Climbing,
|
|
Types.Cranky,
|
|
Types.Funky,
|
|
Types.Candy,
|
|
Types.Snide,
|
|
):
|
|
level = "DK Isles"
|
|
elif "Japes" in location.name:
|
|
level = "Jungle Japes"
|
|
elif "Aztec" in location.name:
|
|
level = "Angry Aztec"
|
|
elif "Factory" in location.name:
|
|
level = "Frantic Factory"
|
|
elif "Galleon" in location.name:
|
|
level = "Gloomy Galleon"
|
|
elif "Forest" in location.name:
|
|
level = "Fungi Forest"
|
|
elif "Caves" in location.name:
|
|
level = "Crystal Caves"
|
|
elif "Castle" in location.name:
|
|
level = "Creepy Castle"
|
|
elif "Helm" in location.name:
|
|
level = "Hideout Helm"
|
|
if self.settings.enemy_drop_rando or location.item != Items.EnemyItem:
|
|
humanspoiler["Items"][level][location.name] = item.name
|
|
humanspoiler[sorted_item_name][self.getItemGroup(location.item)][location.name] = item.name
|
|
if not self.settings.enemy_drop_rando:
|
|
del humanspoiler[sorted_item_name]["Enemy Drops"]
|
|
if self.settings.enemy_rando:
|
|
placement_dict = {}
|
|
for map_id in self.enemy_rando_data:
|
|
map_name = Maps(map_id).name
|
|
map_dict = {}
|
|
for enemy in self.enemy_rando_data[map_id]:
|
|
map_dict[enemy["location"]] = EnemyMetaData[enemy["enemy"]].name
|
|
placement_dict[map_name] = map_dict
|
|
if not self.settings.enemy_drop_rando:
|
|
humanspoiler["Enemy Placement (Stringified JSON)"] = json.dumps(placement_dict)
|
|
else:
|
|
humanspoiler["Enemy Placement"] = placement_dict
|
|
humanspoiler["Bosses"] = {}
|
|
if self.settings.boss_location_rando:
|
|
shuffled_bosses = OrderedDict()
|
|
for i in range(7):
|
|
shuffled_bosses["".join(map(lambda x: x if x.islower() else " " + x, Levels(i).name)).strip()] = boss_map_names.get(self.settings.boss_maps[i], Maps(self.settings.boss_maps[i]).name)
|
|
humanspoiler["Bosses"]["Shuffled Boss Order"] = shuffled_bosses
|
|
|
|
humanspoiler["Bosses"]["King Kut Out Properties"] = {}
|
|
if self.settings.boss_kong_rando:
|
|
shuffled_boss_kongs = OrderedDict()
|
|
for i in range(7):
|
|
shuffled_boss_kongs["".join(map(lambda x: x if x.islower() else " " + x, Levels(i).name)).strip()] = Kongs(self.settings.boss_kongs[i]).name.capitalize()
|
|
humanspoiler["Bosses"]["Shuffled Boss Kongs"] = shuffled_boss_kongs
|
|
kutout_order = ""
|
|
for kong in self.settings.kutout_kongs:
|
|
kutout_order = kutout_order + Kongs(kong).name.capitalize() + ", "
|
|
humanspoiler["Bosses"]["King Kut Out Properties"]["Shuffled Kutout Kong Order"] = kutout_order
|
|
|
|
if HardBossesEnabled(self.settings, HardBossesSelected.kut_out_phase_rando):
|
|
phase_names = []
|
|
for phase in self.settings.kko_phase_order:
|
|
phase_names.append(f"Phase {phase+1}")
|
|
humanspoiler["Bosses"]["King Kut Out Properties"]["Shuffled Kutout Phases"] = ", ".join(phase_names)
|
|
|
|
if self.settings.bonus_barrels == MinigameBarrels.selected and len(self.settings.minigames_list_selected) > 0:
|
|
selected_minigames = []
|
|
for minigame in self.settings.minigames_list_selected:
|
|
selected_minigames.append(minigame.name)
|
|
humanspoiler["Selected Minigames"] = selected_minigames
|
|
if (
|
|
self.settings.bonus_barrels in (MinigameBarrels.random, MinigameBarrels.selected)
|
|
or self.settings.helm_barrels == MinigameBarrels.random
|
|
or self.settings.training_barrels_minigames == MinigameBarrels.random
|
|
):
|
|
shuffled_barrels = OrderedDict()
|
|
for location, minigame in self.shuffled_barrel_data.items():
|
|
if location in HelmMinigameLocations and self.settings.helm_barrels == MinigameBarrels.skip:
|
|
continue
|
|
if location in TrainingMinigameLocations and self.settings.training_barrels_minigames == MinigameBarrels.skip:
|
|
continue
|
|
if location not in HelmMinigameLocations and location not in TrainingMinigameLocations and self.settings.bonus_barrels == MinigameBarrels.skip:
|
|
continue
|
|
shuffled_barrels[self.LocationList[location].name] = MinigameRequirements[minigame].name
|
|
if len(shuffled_barrels) > 0:
|
|
humanspoiler["Shuffled Bonus Barrels"] = shuffled_barrels
|
|
|
|
if self.settings.kasplat_rando:
|
|
humanspoiler["Shuffled Kasplats"] = self.human_kasplats
|
|
if self.settings.random_fairies:
|
|
humanspoiler["Shuffled Banana Fairies"] = self.fairy_locations_human
|
|
if self.settings.random_patches:
|
|
humanspoiler["Shuffled Dirt Patches"] = self.human_patches
|
|
if self.settings.random_crates:
|
|
humanspoiler["Shuffled Melon Crates"] = self.human_crates
|
|
humanspoiler["Settings"]["Shuffled Bananaport Levels"] = False
|
|
if self.settings.bananaport_placement_rando != ShufflePortLocations.off and self.settings.bananaport_rando == BananaportRando.off:
|
|
shuffled_warp_levels = [x.name for x in self.settings.warp_level_list_selected]
|
|
if len(shuffled_warp_levels) == 0:
|
|
humanspoiler["Settings"]["Shuffled Bananaport Levels"] = True
|
|
else:
|
|
humanspoiler["Settings"]["Shuffled Bananaport Levels"] = shuffled_warp_levels
|
|
humanspoiler["Shuffled Bananaport Locations"] = self.human_warps
|
|
if self.settings.bananaport_rando != BananaportRando.off:
|
|
humanspoiler["Shuffled Bananaport Connections (Source -> Destination)"] = self.human_warp_locations
|
|
if self.settings.wrinkly_location_rando:
|
|
prog_hint_setting = self.settings.progressive_hint_item
|
|
item_types = self.settings.shuffled_location_types
|
|
if prog_hint_setting == ProgressiveHintItem.off or Types.Hint in item_types:
|
|
humanspoiler["Wrinkly Door Locations"] = self.human_hint_doors
|
|
if self.settings.tns_location_rando:
|
|
humanspoiler["T&S Portal Locations"] = self.human_portal_doors
|
|
if self.settings.dk_portal_location_rando_v2 != DKPortalRando.off:
|
|
humanspoiler["DK Portal Locations"] = self.human_entry_doors
|
|
if self.settings.crown_placement_rando:
|
|
humanspoiler["Battle Arena Locations"] = self.human_crowns
|
|
if self.settings.switchsanity:
|
|
ss_data = {}
|
|
ss_name_data = {
|
|
Kongs.donkey: {
|
|
SwitchType.SlamSwitch: "Donkey Slam Switch",
|
|
SwitchType.GunSwitch: "Coconut Switch",
|
|
SwitchType.InstrumentPad: "Bongos Pad",
|
|
SwitchType.PadMove: "Baboon Blast Pad",
|
|
SwitchType.MiscActivator: "Gorilla Grab Lever",
|
|
},
|
|
Kongs.diddy: {
|
|
SwitchType.SlamSwitch: "Diddy Slam Switch",
|
|
SwitchType.GunSwitch: "Peanut Switch",
|
|
SwitchType.InstrumentPad: "Guitar Pad",
|
|
SwitchType.PadMove: "Simian Spring Pad",
|
|
SwitchType.MiscActivator: "Gong",
|
|
},
|
|
Kongs.lanky: {
|
|
SwitchType.SlamSwitch: "Lanky Slam Switch",
|
|
SwitchType.GunSwitch: "Grape Switch",
|
|
SwitchType.InstrumentPad: "Trombone Pad",
|
|
SwitchType.PadMove: "Baboon Balloon Pad",
|
|
},
|
|
Kongs.tiny: {
|
|
SwitchType.SlamSwitch: "Tiny Slam Switch",
|
|
SwitchType.GunSwitch: "Feather Switch",
|
|
SwitchType.InstrumentPad: "Saxophone Pad",
|
|
SwitchType.PadMove: "Monkeyport Pad",
|
|
},
|
|
Kongs.chunky: {
|
|
SwitchType.SlamSwitch: "Chunky Slam Switch",
|
|
SwitchType.GunSwitch: "Pineapple Switch",
|
|
SwitchType.InstrumentPad: "Triangle Pad",
|
|
SwitchType.PadMove: "Gorilla Gone Pad",
|
|
},
|
|
}
|
|
for slot in self.settings.switchsanity_data.values():
|
|
ss_data[slot.name] = ss_name_data[slot.kong][slot.switch_type]
|
|
humanspoiler["Switchsanity"] = ss_data
|
|
level_dict = {
|
|
Levels.DKIsles: "DK Isles",
|
|
Levels.JungleJapes: "Jungle Japes",
|
|
Levels.AngryAztec: "Angry Aztec",
|
|
Levels.FranticFactory: "Frantic Factory",
|
|
Levels.GloomyGalleon: "Gloomy Galleon",
|
|
Levels.FungiForest: "Fungi Forest",
|
|
Levels.CrystalCaves: "Crystal Caves",
|
|
Levels.CreepyCastle: "Creepy Castle",
|
|
}
|
|
if self.settings.shuffle_shops:
|
|
shop_location_dict = {}
|
|
for level in self.shuffled_shop_locations:
|
|
level_name = "Unknown Level"
|
|
|
|
shop_dict = {
|
|
Regions.CrankyGeneric: "Cranky",
|
|
Regions.CandyGeneric: "Candy",
|
|
Regions.FunkyGeneric: "Funky",
|
|
Regions.Snide: "Snide",
|
|
}
|
|
if level in level_dict:
|
|
level_name = level_dict[level]
|
|
for shop in self.shuffled_shop_locations[level]:
|
|
location_name = "Unknown Shop"
|
|
shop_name = "Unknown Shop"
|
|
new_shop = self.shuffled_shop_locations[level][shop]
|
|
if shop in shop_dict:
|
|
location_name = shop_dict[shop]
|
|
if new_shop in shop_dict:
|
|
shop_name = shop_dict[new_shop]
|
|
shop_location_dict[f"{level_name} - {location_name}"] = shop_name
|
|
humanspoiler["Shop Locations"] = shop_location_dict
|
|
for spoiler_dict in ("Items", "Bosses"):
|
|
is_empty = True
|
|
for y in humanspoiler[spoiler_dict]:
|
|
if humanspoiler[spoiler_dict][y] != {}:
|
|
is_empty = False
|
|
if is_empty:
|
|
del humanspoiler[spoiler_dict]
|
|
|
|
if self.settings.cb_rando_enabled:
|
|
human_cb_type_map = {"cb": " Bananas", "balloons": " Balloons"}
|
|
humanspoiler["Colored Banana Locations"] = {}
|
|
cb_levels = []
|
|
level_dict = {
|
|
Levels.DKIsles: "DK Isles",
|
|
Levels.JungleJapes: "Jungle Japes",
|
|
Levels.AngryAztec: "Angry Aztec",
|
|
Levels.FranticFactory: "Frantic Factory",
|
|
Levels.GloomyGalleon: "Gloomy Galleon",
|
|
Levels.FungiForest: "Fungi Forest",
|
|
Levels.CrystalCaves: "Crystal Caves",
|
|
Levels.CreepyCastle: "Creepy Castle",
|
|
}
|
|
cb_levels = [name for lvl, name in level_dict.items() if IsItemSelected(self.settings.cb_rando_enabled, self.settings.cb_rando_list_selected, lvl)]
|
|
cb_kongs = ["Donkey", "Diddy", "Lanky", "Tiny", "Chunky"]
|
|
for lvl in cb_levels:
|
|
for kng in cb_kongs:
|
|
humanspoiler["Colored Banana Locations"][f"{lvl} {kng}"] = {"Balloons": [], "Bananas": []}
|
|
for group in self.cb_placements:
|
|
lvl_name = level_dict[group["level"]]
|
|
map_name = "".join(map(lambda x: x if x.islower() else " " + x, Maps(group["map"]).name)).strip()
|
|
join_combos = ["2 D Ship", "5 D Ship", "5 D Temple"]
|
|
for combo in join_combos:
|
|
if combo in map_name:
|
|
map_name = map_name.replace(combo, combo.replace(" ", ""))
|
|
humanspoiler["Colored Banana Locations"][f"{lvl_name} {NameFromKong(group['kong'])}"][human_cb_type_map[group["type"]].strip()].append(f"{map_name.strip()}: {group['name']}")
|
|
if self.settings.coin_rando:
|
|
humanspoiler["Coin Locations"] = {}
|
|
coin_levels = ["Japes", "Aztec", "Factory", "Galleon", "Fungi", "Caves", "Castle", "Isles"]
|
|
coin_kongs = ["Donkey", "Diddy", "Lanky", "Tiny", "Chunky"]
|
|
for lvl in coin_levels:
|
|
for kng in coin_kongs:
|
|
humanspoiler["Coin Locations"][f"{lvl} {kng}"] = []
|
|
for group in self.coin_placements:
|
|
lvl_name = level_dict[group["level"]]
|
|
idx = 1
|
|
if group["level"] == Levels.FungiForest:
|
|
idx = 0
|
|
map_name = "".join(map(lambda x: x if x.islower() else " " + x, Maps(group["map"]).name)).strip()
|
|
join_combos = ["2 D Ship", "5 D Ship", "5 D Temple"]
|
|
for combo in join_combos:
|
|
if combo in map_name:
|
|
map_name = map_name.replace(combo, combo.replace(" ", ""))
|
|
humanspoiler["Coin Locations"][f"{lvl_name.split(' ')[idx]} {NameFromKong(group['kong'])}"].append(f"{map_name.strip()}: {group['name']}")
|
|
|
|
# Playthrough data
|
|
humanspoiler["Playthrough"] = self.playthrough
|
|
|
|
# Woth data
|
|
humanspoiler["Way of the Hoard"] = self.woth
|
|
humanspoiler["WotH Paths"] = {}
|
|
slamCount = 0
|
|
pearlCount = 0
|
|
for loc, path in self.woth_paths.items():
|
|
destination_item = ItemList[self.LocationList[loc].item]
|
|
path_dict = {}
|
|
for path_loc_id in path:
|
|
path_location = self.LocationList[path_loc_id]
|
|
path_item = ItemList[path_location.item]
|
|
path_dict[path_location.name] = path_item.name
|
|
extra = ""
|
|
if self.LocationList[loc].item == Items.ProgressiveSlam:
|
|
slamCount += 1
|
|
extra = " " + str(slamCount)
|
|
if self.LocationList[loc].item == Items.Pearl:
|
|
pearlCount += 1
|
|
extra = " " + str(pearlCount)
|
|
humanspoiler["WotH Paths"][destination_item.name + extra] = path_dict
|
|
for map_id, path in self.krool_paths.items():
|
|
path_dict = {}
|
|
for path_loc_id in path:
|
|
path_location = self.LocationList[path_loc_id]
|
|
path_item = ItemList[path_location.item]
|
|
path_dict[path_location.name] = path_item.name
|
|
phase_name = boss_map_names.get(map_id, Maps(map_id).name)
|
|
humanspoiler["WotH Paths"][phase_name] = path_dict
|
|
if self.settings.win_condition_item == WinConditionComplex.dk_rap_items:
|
|
for verse_name, path in self.rap_win_con_paths.items():
|
|
path_dict = {}
|
|
for path_loc_id in path:
|
|
path_location = self.LocationList[path_loc_id]
|
|
path_item = ItemList[path_location.item]
|
|
path_dict[path_location.name] = path_item.name
|
|
humanspoiler["WotH Paths"][verse_name] = path_dict
|
|
humanspoiler["Other Paths"] = {}
|
|
for loc, path in self.other_paths.items():
|
|
destination_item = ItemList[self.LocationList[loc].item]
|
|
path_dict = {}
|
|
for path_loc_id in path:
|
|
path_location = self.LocationList[path_loc_id]
|
|
path_item = ItemList[path_location.item]
|
|
path_dict[path_location.name] = path_item.name
|
|
extra = ""
|
|
if self.LocationList[loc].item == Items.ProgressiveSlam:
|
|
slamCount += 1
|
|
extra = " " + str(slamCount)
|
|
if self.LocationList[loc].item == Items.Pearl:
|
|
pearlCount += 1
|
|
extra = " " + str(pearlCount)
|
|
humanspoiler["Other Paths"][destination_item.name + extra] = path_dict
|
|
|
|
if self.settings.shuffle_loading_zones == ShuffleLoadingZones.levels:
|
|
# Just show level order
|
|
shuffled_exits = OrderedDict()
|
|
lobby_entrance_order = {
|
|
Transitions.IslesMainToJapesLobby: Levels.JungleJapes,
|
|
Transitions.IslesMainToAztecLobby: Levels.AngryAztec,
|
|
Transitions.IslesMainToFactoryLobby: Levels.FranticFactory,
|
|
Transitions.IslesMainToGalleonLobby: Levels.GloomyGalleon,
|
|
Transitions.IslesMainToForestLobby: Levels.FungiForest,
|
|
Transitions.IslesMainToCavesLobby: Levels.CrystalCaves,
|
|
Transitions.IslesMainToCastleLobby: Levels.CreepyCastle,
|
|
Transitions.IslesMainToHelmLobby: Levels.HideoutHelm,
|
|
}
|
|
lobby_exit_order = {
|
|
Transitions.IslesJapesLobbyToMain: Levels.JungleJapes,
|
|
Transitions.IslesAztecLobbyToMain: Levels.AngryAztec,
|
|
Transitions.IslesFactoryLobbyToMain: Levels.FranticFactory,
|
|
Transitions.IslesGalleonLobbyToMain: Levels.GloomyGalleon,
|
|
Transitions.IslesForestLobbyToMain: Levels.FungiForest,
|
|
Transitions.IslesCavesLobbyToMain: Levels.CrystalCaves,
|
|
Transitions.IslesCastleLobbyToMain: Levels.CreepyCastle,
|
|
Transitions.IslesHelmLobbyToMain: Levels.HideoutHelm,
|
|
}
|
|
for transition, vanilla_level in lobby_entrance_order.items():
|
|
shuffled_level = lobby_exit_order[self.shuffled_exit_data[transition].reverse]
|
|
shuffled_exits[vanilla_level.name] = shuffled_level.name
|
|
humanspoiler["Shuffled Level Order"] = shuffled_exits
|
|
elif self.settings.shuffle_loading_zones != ShuffleLoadingZones.none:
|
|
# Show full shuffled_exits data if more than just levels are shuffled
|
|
shuffled_exits = OrderedDict()
|
|
level_starts = {
|
|
"DK Isles": [
|
|
"DK Isles",
|
|
"Japes Lobby",
|
|
"Aztec Lobby",
|
|
"Factory Lobby",
|
|
"Galleon Lobby",
|
|
"Fungi Lobby",
|
|
"Caves Lobby",
|
|
"Castle Lobby",
|
|
"Helm Lobby",
|
|
"Snide's Room",
|
|
"Training Grounds",
|
|
"Banana Fairy Isle",
|
|
"DK's Treehouse",
|
|
"K-Lumsy",
|
|
],
|
|
"Jungle Japes": ["Jungle Japes"],
|
|
"Angry Aztec": ["Angry Aztec"],
|
|
"Frantic Factory": ["Frantic Factory"],
|
|
"Gloomy Galleon": ["Gloomy Galleon"],
|
|
"Fungi Forest": ["Fungi Forest"],
|
|
"Crystal Caves": ["Crystal Caves"],
|
|
"Creepy Castle": ["Creepy Castle"],
|
|
"Hideout Helm": ["Hideout Helm"],
|
|
}
|
|
level_data = {"Other": {}}
|
|
for level in level_starts:
|
|
level_data[level] = {}
|
|
for exit, dest in self.shuffled_exit_data.items():
|
|
level_name = "Other"
|
|
for level in level_starts:
|
|
for search_name in level_starts[level]:
|
|
if dest.spoilerName.find(search_name) == 0:
|
|
level_name = level
|
|
shuffled_exits[ShufflableExits[exit].name] = dest.spoilerName
|
|
level_data[level_name][ShufflableExits[exit].name] = dest.spoilerName
|
|
humanspoiler["Shuffled Exits"] = shuffled_exits
|
|
humanspoiler["Shuffled Exits (Sorted by destination)"] = level_data
|
|
if self.settings.has_password:
|
|
PASS_NAMES = ["ERROR", "Up", "Down", "Left", "Right", "Z", "Start"]
|
|
humanspoiler["Password"] = " ".join([PASS_NAMES[x] for x in self.settings.password])
|
|
if self.settings.alter_switch_allocation:
|
|
SLAM_NAMES = ["No Slam", "Simian Slam", "Super Simian Slam", "Super Duper Simian Slam"]
|
|
humanspoiler["Level Switch Strength"] = {
|
|
"Jungle Japes": SLAM_NAMES[self.settings.switch_allocation[Levels.JungleJapes]],
|
|
"Angry Aztec": SLAM_NAMES[self.settings.switch_allocation[Levels.AngryAztec]],
|
|
"Frantic Factory": SLAM_NAMES[self.settings.switch_allocation[Levels.FranticFactory]],
|
|
"Gloomy Galleon": SLAM_NAMES[self.settings.switch_allocation[Levels.GloomyGalleon]],
|
|
"Fungi Forest": SLAM_NAMES[self.settings.switch_allocation[Levels.FungiForest]],
|
|
"Crystal Caves": SLAM_NAMES[self.settings.switch_allocation[Levels.CrystalCaves]],
|
|
"Creepy Castle": SLAM_NAMES[self.settings.switch_allocation[Levels.CreepyCastle]],
|
|
}
|
|
if self.settings.shuffle_helm_location:
|
|
humanspoiler["Level Switch Strength"]["Hideout Helm"] = SLAM_NAMES[self.settings.switch_allocation[Levels.HideoutHelm]]
|
|
|
|
if len(self.microhints) > 0:
|
|
human_microhints = {}
|
|
for name, hint in self.microhints.items():
|
|
filtered_hint = hint.replace("\x04", "")
|
|
filtered_hint = filtered_hint.replace("\x05", "")
|
|
filtered_hint = filtered_hint.replace("\x06", "")
|
|
filtered_hint = filtered_hint.replace("\x07", "")
|
|
filtered_hint = filtered_hint.replace("\x08", "")
|
|
filtered_hint = filtered_hint.replace("\x09", "")
|
|
filtered_hint = filtered_hint.replace("\x0a", "")
|
|
filtered_hint = filtered_hint.replace("\x0b", "")
|
|
filtered_hint = filtered_hint.replace("\x0c", "")
|
|
filtered_hint = filtered_hint.replace("\x0d", "")
|
|
human_microhints[name] = filtered_hint
|
|
humanspoiler["Direct Item Hints"] = human_microhints
|
|
if len(self.hint_list) > 0:
|
|
human_hint_list = {}
|
|
# Here it filters out the coloring from the hints to make it actually readable in the spoiler log
|
|
for name, hint in self.hint_list.items():
|
|
filtered_hint = hint.replace("\x04", "")
|
|
filtered_hint = filtered_hint.replace("\x05", "")
|
|
filtered_hint = filtered_hint.replace("\x06", "")
|
|
filtered_hint = filtered_hint.replace("\x07", "")
|
|
filtered_hint = filtered_hint.replace("\x08", "")
|
|
filtered_hint = filtered_hint.replace("\x09", "")
|
|
filtered_hint = filtered_hint.replace("\x0a", "")
|
|
filtered_hint = filtered_hint.replace("\x0b", "")
|
|
filtered_hint = filtered_hint.replace("\x0c", "")
|
|
filtered_hint = filtered_hint.replace("\x0d", "")
|
|
human_hint_list[name] = filtered_hint
|
|
humanspoiler["Wrinkly Hints"] = human_hint_list
|
|
# humanspoiler["Unhinted Score"] = self.unhinted_score
|
|
# humanspoiler["Potentially Awful Locations"] = {}
|
|
# for location_description in self.poor_scoring_locations:
|
|
# humanspoiler["Potentially Awful Locations"][location_description] = self.poor_scoring_locations[location_description]
|
|
# if hasattr(self, "hint_swap_advisory"):
|
|
# humanspoiler["Hint Swap Advisory"] = self.hint_swap_advisory
|
|
self.json = json.dumps(humanspoiler, indent=4)
|
|
|
|
def UpdateKasplats(self, kasplat_map: Dict[Locations, Kongs]) -> None:
|
|
"""Update kasplat data."""
|
|
for kasplat, kong in kasplat_map.items():
|
|
# Get kasplat info
|
|
location = self.LocationList[kasplat]
|
|
mapId = location.map
|
|
original = location.kong
|
|
self.human_kasplats[location.name] = NameFromKong(kong)
|
|
map = None
|
|
# See if map already exists in enemy_replacements
|
|
for m in self.enemy_replacements:
|
|
if m["container_map"] == mapId:
|
|
map = m
|
|
break
|
|
# If not, create it
|
|
if map is None:
|
|
map = {"container_map": mapId}
|
|
self.enemy_replacements.append(map)
|
|
# Create kasplat_swaps section if doesn't exist
|
|
if "kasplat_swaps" not in map:
|
|
map["kasplat_swaps"] = []
|
|
# Create swap entry and add to map
|
|
swap = {"vanilla_location": original, "replace_with": kong}
|
|
map["kasplat_swaps"].append(swap)
|
|
|
|
def UpdateBarrels(self) -> None:
|
|
"""Update list of shuffled barrel minigames."""
|
|
self.shuffled_barrel_data = {}
|
|
for location, minigame in [(key, value.minigame) for (key, value) in BarrelMetaData.items()]:
|
|
self.shuffled_barrel_data[location] = minigame
|
|
|
|
def UpdateExits(self) -> None:
|
|
"""Update list of shuffled exits."""
|
|
self.shuffled_exit_data = {}
|
|
containerMaps = {}
|
|
for key, exit in ShufflableExits.items():
|
|
if exit.shuffled:
|
|
try:
|
|
vanillaBack = exit.back
|
|
shuffledBack = ShufflableExits[exit.shuffledId].back
|
|
self.shuffled_exit_data[key] = shuffledBack
|
|
containerMapId = GetMapId(self.settings, exit.region)
|
|
if containerMapId not in containerMaps:
|
|
containerMaps[containerMapId] = {"container_map": containerMapId, "zones": []} # DK Isles
|
|
loading_zone_mapping = {}
|
|
loading_zone_mapping["vanilla_map"] = GetMapId(self.settings, vanillaBack.regionId)
|
|
loading_zone_mapping["vanilla_exit"] = GetExitId(vanillaBack)
|
|
loading_zone_mapping["new_map"] = GetMapId(self.settings, shuffledBack.regionId)
|
|
loading_zone_mapping["new_exit"] = GetExitId(shuffledBack)
|
|
containerMaps[containerMapId]["zones"].append(loading_zone_mapping)
|
|
except Exception as ex:
|
|
print("Exit Update Error with:")
|
|
print(ex)
|
|
for key, containerMap in containerMaps.items():
|
|
self.shuffled_exit_instructions.append(containerMap)
|
|
|
|
def UpdateLocations(self, locations: Dict[Locations, Location]) -> None:
|
|
"""Update location list for what was produced by the fill."""
|
|
self.location_data = {}
|
|
self.shuffled_kong_placement = {}
|
|
# Go ahead and set starting kong
|
|
startkong = {"kong": self.settings.starting_kong, "write": 0x151}
|
|
trainingGrounds = {"locked": startkong}
|
|
self.shuffled_kong_placement["TrainingGrounds"] = trainingGrounds
|
|
# Write additional starting kongs to empty cages, if any
|
|
emptyCages = [x for x in [Locations.DiddyKong, Locations.LankyKong, Locations.TinyKong, Locations.ChunkyKong] if x not in self.settings.kong_locations]
|
|
for emptyCage in emptyCages:
|
|
self.WriteKongPlacement(emptyCage, Items.NoItem)
|
|
|
|
# Loop through locations and set necessary data
|
|
for id, location in locations.items():
|
|
# (There must be an item here) AND (It must not be a constant item expected to be here) AND (It must be in a location not handled by the full item rando shuffler)
|
|
if location.item is not None and location.item is not Items.NoItem and not location.constant and location.type not in self.settings.shuffled_location_types:
|
|
self.location_data[id] = location.item
|
|
if location.type == Types.Shop:
|
|
# Get indices from the location
|
|
shop_index = 0 # cranky
|
|
if location.movetype in [MoveTypes.Guns, MoveTypes.AmmoBelt]:
|
|
shop_index = 1 # funky
|
|
elif location.movetype == MoveTypes.Instruments:
|
|
shop_index = 2 # candy
|
|
kong_indices = [location.kong]
|
|
if location.kong == Kongs.any:
|
|
kong_indices = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
|
|
level_index = location.level
|
|
# Isles level index is 8, but we need it to be 7 to fit it in move_data
|
|
if level_index == 8:
|
|
level_index = 7
|
|
# Use the item to find the data to write
|
|
updated_item = ItemList[location.item]
|
|
move_type = updated_item.movetype
|
|
# Determine Price
|
|
price = 0
|
|
if id in self.settings.prices:
|
|
price = self.settings.prices[id]
|
|
# Vanilla prices are by item, not by location
|
|
if self.settings.random_prices == RandomPrices.vanilla:
|
|
price = self.settings.prices[location.item]
|
|
# Moves that are set with a single flag (e.g. training barrels, shockwave) are handled differently
|
|
if move_type == MoveTypes.Flag:
|
|
for kong_index in kong_indices:
|
|
self.move_data[0][shop_index][kong_index][level_index] = {
|
|
"move_type": "flag",
|
|
"flag": updated_item.flag,
|
|
"price": price,
|
|
}
|
|
# This is for every other move typically purchased in a shop
|
|
else:
|
|
move_level = updated_item.index - 1
|
|
move_kong = updated_item.kong
|
|
for kong_index in kong_indices:
|
|
# print(f"Shop {shop_index}, Kong {kong_index}, Level {level_index} | Move: {move_type} lvl {move_level} for kong {move_kong}")
|
|
if (
|
|
move_type == MoveTypes.Slam
|
|
or move_type == MoveTypes.AmmoBelt
|
|
or (move_type == MoveTypes.Guns and move_level > 0)
|
|
or (move_type == MoveTypes.Instruments and move_level > 0)
|
|
):
|
|
move_kong = kong_index
|
|
self.move_data[0][shop_index][kong_index][level_index] = {
|
|
"move_type": ["special", "slam", "gun", "ammo_belt", "instrument"][move_type],
|
|
"move_lvl": move_level,
|
|
"move_kong": move_kong,
|
|
"price": price,
|
|
}
|
|
elif location.type == Types.Kong:
|
|
self.WriteKongPlacement(id, location.item)
|
|
elif location.type == Types.TrainingBarrel and self.settings.training_barrels != TrainingBarrels.normal:
|
|
# Use the item to find the data to write
|
|
updated_item = ItemList[location.item]
|
|
move_type = updated_item.movetype
|
|
# Determine Price to be zero because this is a training barrel
|
|
price = 0
|
|
# Moves that are set with a single flag (e.g. training barrels, shockwave) are handled differently
|
|
if move_type == MoveTypes.Flag:
|
|
self.move_data[1].append({"move_type": "flag", "flag": updated_item.flag, "price": price})
|
|
# This is for every other move typically purchased in a shop
|
|
else:
|
|
move_level = updated_item.index - 1
|
|
move_kong = updated_item.kong
|
|
self.move_data[1].append(
|
|
{
|
|
"move_type": ["special", "slam", "gun", "ammo_belt", "instrument"][move_type],
|
|
"move_lvl": move_level,
|
|
"move_kong": move_kong % 5, # Shared moves are 5 but should be 0
|
|
"price": price,
|
|
}
|
|
)
|
|
# Clean up the default value from this list
|
|
for x in range(len(self.move_data[1])):
|
|
if self.move_data[1][x] == {"move_type": None}:
|
|
del self.move_data[1][x]
|
|
break
|
|
elif location.type == Types.Shockwave and self.settings.shockwave_status != ShockwaveStatus.vanilla:
|
|
# Use the item to find the data to write
|
|
updated_item = ItemList[location.item]
|
|
move_type = updated_item.movetype
|
|
# Determine Price to be zero because BFI is free
|
|
price = 0
|
|
# Moves that are set with a single flag (e.g. training barrels, shockwave) are handled differently
|
|
if move_type == MoveTypes.Flag:
|
|
self.move_data[2] = [{"move_type": "flag", "flag": updated_item.flag, "price": price}]
|
|
# This is for every other move typically purchased in a shop
|
|
else:
|
|
move_level = updated_item.index - 1
|
|
move_kong = updated_item.kong
|
|
self.move_data[2] = [
|
|
{
|
|
"move_type": ["special", "slam", "gun", "ammo_belt", "instrument"][move_type],
|
|
"move_lvl": move_level,
|
|
"move_kong": move_kong % 5, # Shared moves are 5 but should be 0
|
|
"price": price,
|
|
}
|
|
]
|
|
# Uncomment for more verbose spoiler with all locations
|
|
# else:
|
|
# self.location_data[id] = Items.NoItem
|
|
|
|
def WriteKongPlacement(self, locationId: Locations, item: Items) -> None:
|
|
"""Write kong placement information for the given kong cage location."""
|
|
locationName = "Jungle Japes"
|
|
unlockKong = self.settings.diddy_freeing_kong
|
|
lockedwrite = 0x152
|
|
puzzlewrite = 0x153
|
|
if locationId == Locations.LankyKong:
|
|
locationName = "Llama Temple"
|
|
unlockKong = self.settings.lanky_freeing_kong
|
|
lockedwrite = 0x154
|
|
puzzlewrite = 0x155
|
|
elif locationId == Locations.TinyKong:
|
|
locationName = "Tiny Temple"
|
|
unlockKong = self.settings.tiny_freeing_kong
|
|
lockedwrite = 0x156
|
|
puzzlewrite = 0x157
|
|
elif locationId == Locations.ChunkyKong:
|
|
locationName = "Frantic Factory"
|
|
unlockKong = self.settings.chunky_freeing_kong
|
|
lockedwrite = 0x158
|
|
puzzlewrite = 0x159
|
|
lockedkong = {}
|
|
lockedkong["kong"] = KongFromItem(item)
|
|
lockedkong["write"] = lockedwrite
|
|
puzzlekong = {"kong": unlockKong, "write": puzzlewrite}
|
|
kongLocation = {"locked": lockedkong, "puzzle": puzzlekong}
|
|
self.shuffled_kong_placement[locationName] = kongLocation
|
|
|
|
def UpdatePlaythrough(self, locations: Dict[Locations, Location], playthroughLocations: List[Sphere]) -> None:
|
|
"""Write playthrough as a list of dicts of location/item pairs."""
|
|
self.playthrough = {}
|
|
i = 0
|
|
for sphere in playthroughLocations:
|
|
newSphere = {}
|
|
newSphere["Available GBs"] = sphere.availableGBs
|
|
sphereLocations = list(map(lambda l: locations[l], sphere.locations))
|
|
sphereLocations.sort(key=self.ScoreLocations)
|
|
for location in sphereLocations:
|
|
newSphere[location.name] = ItemList[location.item].name
|
|
self.playthrough[i] = newSphere
|
|
i += 1
|
|
|
|
def UpdateWoth(self, locations: Dict[Locations, Location], wothLocations: List[Union[Locations, int]]) -> None:
|
|
"""Write woth locations as a dict of location/item pairs."""
|
|
self.woth = {}
|
|
self.woth_locations = wothLocations
|
|
for locationId in wothLocations:
|
|
location = locations[locationId]
|
|
self.woth[location.name] = ItemList[location.item].name
|
|
|
|
def ScoreLocations(self, location: Location) -> int:
|
|
"""Score a location with the given settings for sorting the Playthrough."""
|
|
# The Banana Hoard is likely in its own sphere but if it's not put it last
|
|
if location == Locations.BananaHoard:
|
|
return 250
|
|
# GBs go last, there's a lot of them but they arent important
|
|
if ItemList[location.item].type == Types.Banana:
|
|
return 100
|
|
win_con_type_table = {
|
|
WinConditionComplex.req_bean: Types.Bean,
|
|
WinConditionComplex.req_bp: Types.Blueprint,
|
|
WinConditionComplex.req_companycoins: Types.NintendoCoin, # Also Types.RarewareCoin
|
|
WinConditionComplex.req_crown: Types.Crown,
|
|
WinConditionComplex.req_fairy: Types.Fairy,
|
|
WinConditionComplex.req_gb: Types.Banana, # Also Types.ToughBanana
|
|
# WinConditionComplex.req_key: Types.Key,
|
|
WinConditionComplex.req_medal: Types.Medal,
|
|
WinConditionComplex.req_pearl: Types.Pearl,
|
|
WinConditionComplex.req_rainbowcoin: Types.RainbowCoin,
|
|
}
|
|
# Win condition items are more important than GBs but less than moves
|
|
if self.settings.win_condition_item in win_con_type_table:
|
|
if ItemList[location.item].type == win_con_type_table[self.settings.win_condition_item]:
|
|
return 10
|
|
if self.settings.win_condition_item == WinConditionComplex.req_companycoins:
|
|
if ItemList[location.item].type == Types.RarewareCoin:
|
|
return 10
|
|
# Kongs are most the single most important thing and should be at the top of spheres
|
|
if ItemList[location.item].type == Types.Kong:
|
|
return 0
|
|
# Keys are best put first
|
|
elif ItemList[location.item].type == Types.Key:
|
|
return 1
|
|
# Moves are pretty important
|
|
elif ItemList[location.item].type == Types.Shop:
|
|
return 2
|
|
# Everything else here is probably something unusual so it's likely important
|
|
else:
|
|
return 3
|
|
|
|
@staticmethod
|
|
def GetKroolKeysRequired(keyEvents: List[Events]) -> List[str]:
|
|
"""Get key names from required key events to print in the spoiler."""
|
|
keys = []
|
|
if Events.JapesKeyTurnedIn in keyEvents:
|
|
keys.append("Jungle Japes Key")
|
|
if Events.AztecKeyTurnedIn in keyEvents:
|
|
keys.append("Angry Aztec Key")
|
|
if Events.FactoryKeyTurnedIn in keyEvents:
|
|
keys.append("Frantic Factory Key")
|
|
if Events.GalleonKeyTurnedIn in keyEvents:
|
|
keys.append("Gloomy Galleon Key")
|
|
if Events.ForestKeyTurnedIn in keyEvents:
|
|
keys.append("Fungi Forest Key")
|
|
if Events.CavesKeyTurnedIn in keyEvents:
|
|
keys.append("Crystal Caves Key")
|
|
if Events.CastleKeyTurnedIn in keyEvents:
|
|
keys.append("Creepy Castle Key")
|
|
if Events.HelmKeyTurnedIn in keyEvents:
|
|
keys.append("Hideout Helm Key")
|
|
return keys
|