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

1707 lines
83 KiB
Python

import gc
import logging
import math
from typing import Any, Dict, TextIO
from BaseClasses import ItemClassification, Location, MultiWorld, Tutorial, Item, Region, Entrance
from Options import Accessibility, OptionError, OptionGroup
from .MrTipText import generate_tip_table
from .TrapText import static_trap_name_table, dynamic_trap_name_table, select_trap_item_name
import settings
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, icon_paths, Type, launch_subprocess
from worlds.generic.Rules import add_rule, set_rule
from . import Options
from .Presets import glover_option_presets
from .Options import DifficultyLogic, GaribLogic, GloverOptions, GaribSorting, StartingBall, VictoryCondition
from .JsonReader import build_data, generate_location_information
from .ItemPool import construct_blank_world_garibs, generate_item_name_to_id, generate_item_name_groups, find_item_data, world_garib_table, decoupled_garib_table, garibsanity_world_table, checkpoint_table, level_event_table, move_table, potion_table, portalsanity_table
from Utils import local_path, visualize_regions, VersionException
from .Hints import create_hints
def run_client():
from .GloverClient import main # lazy import
launch_subprocess(main)
components.append(Component("Glover Client", func=run_client, component_type=Type.CLIENT,
icon='Glover Icon',
description="Glover's N64 AP. Shazam!"))
icon_paths['Glover Icon'] = "ap:worlds.glover/assets/icon.png"
class GloverSettings(settings.Group):
class RomPath(settings.OptionalUserFilePath):
"""File path of the Glover (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 GloverItem(Item):
#Start at 650000
game: str = "Glover"
#Fake names for traps
fake_name : str | None = None
@property
def hint_text(self) -> str:
name_for_use = self.name
if self.fake_name != None:
name_for_use = self.fake_name
return getattr(self, "_hint_text", name_for_use.replace("_", " ").replace("-", " "))
@property
def pedestal_hint_text(self) -> str:
name_for_use = self.name
if self.fake_name != None:
name_for_use = self.fake_name
return getattr(self, "_pedestal_hint_text", name_for_use.replace("_", " ").replace("-", " "))
class GloverLocation(Location):
game : str = "Glover"
class GloverWeb(WebWorld):
englishTut = Tutorial("",
"""A guide for setting up Archipelago Glover on your computer.""",
"English",
"setup_en.md",
"setup/en",
["Smg065"])
tutorials = [englishTut]
bug_report_page = "https://github.com/Smg065/GloverArchipelago/issues"
option_groups = [
OptionGroup("Victory Conditions", [
Options.VictoryCondition,
Options.RequiredCrystals,
Options.GoldenGaribCount,
Options.GoldenGaribRequirement
]),
OptionGroup("Difficulty", [
Options.DifficultyLogic,
Options.EasyBallWalk
]),
OptionGroup("Game Setup", [
Options.StartingBall,
Options.RandomizeJump,
Options.IncludePowerBall
]),
OptionGroup("Garibs", [
Options.GaribLogic,
Options.GaribSorting,
Options.GaribOrderOverrides,
Options.RandomGaribSounds,
Options.MadGaribs,
Options.DisableGaribItems
]),
OptionGroup("Levels", [
Options.EnableBonuses,
Options.EntranceRandomizer,
Options.EntranceOverrides,
Options.OpenWorlds,
Options.OpenLevels,
Options.Portalsanity
]),
OptionGroup("Checkpoints", [
Options.SpawningCheckpointRandomizer,
Options.CheckpointOverrides
]),
OptionGroup("Locations", [
Options.CheckpointsChecks,
Options.SwitchesChecks,
Options.MrTipChecks,
Options.Enemysanity,
Options.Insectity,
Options.TotalScores,
Options.LevelScores
]),
OptionGroup("Hints", [
Options.MrHints,
Options.MrTipTextDisplay,
Options.MrTipScouts,
Options.ChickenHints
]),
OptionGroup("Filler and Trap Setup", [
Options.FillerDuration,
Options.TrapPercentage,
Options.ExtraGaribsValue,
]),
OptionGroup("Filler Weights", [
Options.FillerExtraGaribsWeight,
Options.FillerChickenSoundWeight,
Options.FillerLifeWeight,
Options.FillerBoomerangBallWeight,
Options.FillerBeachballWeight,
Options.FillerHerculesPotionWeight,
Options.FillerHelicopterPotionWeight,
Options.FillerSpeedPotionWeight,
Options.FillerFrogPotionWeight,
Options.FillerDeathPotionWeight,
Options.FillerStickyPotionWeight,
Options.FillerBigBallWeight,
Options.FillerLowGravityWeight
]),
OptionGroup("Trap Weights", [
Options.TrapFrogWeight,
Options.TrapCursedBallWeight,
Options.TrapBecomesCrystalWeight,
Options.TrapCameraRotateWeight,
Options.TrapFishEyeWeight,
Options.TrapEnemyBallWeight,
Options.TrapControlBallWeight,
Options.TrapInvisiballWeight#,
#Options.TrapTipWeight
])
]
theme = "grassFlowers"
location_descriptions = {
"Switch" : "An interactable element in Glover, that usually involves either getting the ball to a spot or fist-slamming the object.",
"Garib" : "A red and yellow card collectable. Can be grouped together or made to be checked individually.",
"Life": "A small symbol of a ball with blue sparkles around it.",
"Checkpoint": "A flat magical portal that hovers above the ground. Making the ball pass through it collects it.",
"Potion": "A bottle or star that, when collected, gives Glover the respective powerup if unlocked.",
"Goal": "The vortex at the end of the stage. In the case of bosses, the red and yellow target platform.",
"Tip": "A floating blue hat that gives advice when you walk up to them and press B.",
"Enemy": "Entities that can hurt or be an obstacle to Glover. Some enemies can only be defeated by knocking them off the map.",
"Insect": "Flying insects that must be eaten as a frog. Ground insects can also be fist-slammed to be defeated."
}
item_descriptions = {
"Balls" : "A type of ball that you can transform the ball into. You will always start with one by default.",
"Garibs" : "Can be grouped together or individual. Getting all of them for a level is either a star mark (or a check in Portalsanity).",
"Checkpoints": "Lets you warp to that checkpoint portal in a level.",
"Potions": "Makes the respective bottle or star power up give Glover the intended effect.",
"Spells": "A one time use instant activation of a potion or star effect.",
"Level Events": "A map-shifting event that is normally triggered by a Switch."
}
options_presets = glover_option_presets
class GloverWorld(World):
"""
Glover is an N64 physics puzzle platforming game.
"""
game: str = "Glover"
version: str = "V1.0"
web = GloverWeb()
topology_present = True
settings: GloverSettings
settings_key = "glover_options"
options_dataclass = GloverOptions
options: GloverOptions
# Universal Tracker related data
ut_can_gen_without_yaml: bool = True
using_ut: bool
visited_worlds: int
#Check/Item Prefixes
world_prefixes = ["Atl", "Crn", "Prt", "Pht", "FoF", "Otw"]
level_prefixes = ["H", "1", "2", "3", "!", "?"]
existing_levels = ["Atl1", "Atl2", "Atl3", "Atl!", "Atl?",
"Crn1", "Crn2", "Crn3", "Crn!", "Crn?",
"Prt1", "Prt2", "Prt3", "Prt!", "Prt?",
"Pht1", "Pht2", "Pht3", "Pht!", "Pht?",
"FoF1", "FoF2", "FoF3", "FoF!", "FoF?",
"Otw1", "Otw2", "Otw3", "Otw!", "Otw?",
"Training"]
group_lists : list[str] = ["Not Crystal",
"Not Bowling",
"Not Bowling or Crystal",
"Sinks",
"Floats",
"Ball Up"]
lua_prefixes = ["AP_ATLANTIS", "AP_CARNIVAL", "AP_PIRATES", "AP_PREHISTORIC", "AP_FORTRESS", "AP_SPACE",
"AP_TRAINING"]
lua_suffixes = ["_L1", "_L2", "_L3", "_BOSS", "_BONUS", "_WORLD"]
item_name_to_id = generate_item_name_to_id(world_prefixes, level_prefixes)
item_name_groups = generate_item_name_groups()
location_name_to_id, location_name_groups = generate_location_information(world_prefixes, level_prefixes)
def collect(self, state, item):
output = super().collect(state, item)
name : str = item.name
#Item group lists
for each_group in self.group_lists:
if name in self.item_name_groups[each_group] and state.count_group(each_group, self.player) == 1:
state.add_item(each_group, self.player)
#You've gotten a ball by beating the level in the 4th gate
if name.endswith("H Ball") and name.startswith(tuple(self.world_prefixes)) and item.code == None:
state.add_item("Returned Balls", self.player)
#Garib counting
if not self.garibs_are_filler:
if self.is_garib_item(name, False):
garibs_number : int = self.get_garib_group_size(name)
if garibs_number >= 0:
state.add_item("Total Garibs", self.player, garibs_number)
return output
def remove(self, state, item):
output = super().remove(state, item)
name : str = item.name
#Item group lists
for each_group in self.group_lists:
if name in self.item_name_groups[each_group] and state.count_group(each_group, self.player) == 0:
state.remove_item(each_group, self.player)
#You've gotten a ball by beating the level in the 4th gate
if name.endswith("H Ball") and name.startswith(tuple(self.world_prefixes)) and item.code == None:
state.remove_item("Returned Balls", self.player)
#Garib counting
if not self.garibs_are_filler:
if self.is_garib_item(name, False):
garibs_number : int = self.get_garib_group_size(name)
if garibs_number >= 0:
state.remove_item("Total Garibs", self.player, garibs_number)
return output
def is_garib_item(self, name : str, include_level_locked : bool = True):
if not include_level_locked and (name.count(" ") >= 2):
return False
return name.endswith("Garib") or name.endswith("Garibs") and name != "Golden Garib" and name != "Locate Garibs"
def get_garib_group_size(self, garibName : str):
if garibName == "Garib":
return 1
nameDigit : str = garibName.removesuffix("s").removesuffix(" Garib")[-2:].removeprefix(" ")
if nameDigit.isdigit():
return int(nameDigit)
return -1
def __init__(self, world, player):
self.version = "V1.0"
self.spawn_checkpoint = [
2,3,3,
4,5,4,
3,3,4,
3,4,4,
3,3,5,
2,1,4]
#Level Portal Randomization
self.wayroom_entrances : list[str] = []
self.overworld_entrances : list[str] = []
for each_world_prefix in self.world_prefixes:
for each_level_prefix in self.level_prefixes:
each_entrance : str = each_world_prefix + each_level_prefix
if each_level_prefix == "H":
self.overworld_entrances.append(each_entrance)
else:
self.wayroom_entrances.append(each_entrance)
self.wayroom_entrances.append("Training")
self.overworld_entrances.append("Well")
#Garib level order table
self.garib_level_order = [
["Atl1", 50],
["Atl2", 60],
["Atl3", 80],
["Atl?", 25],
["Crn1", 65],
["Crn2", 80],
["Crn3", 80],
["Crn?", 20],
["Prt1", 70],
["Prt2", 60],
["Prt3", 80],
["Prt?", 50],
["Pht1", 80],
["Pht2", 80],
["Pht3", 80],
["Pht?", 60],
["FoF1", 60],
["FoF2", 60],
["FoF3", 70],
["FoF?", 56],
["Otw1", 50],
["Otw2", 50],
["Otw3", 80],
["Otw?", 50]
]
#Extra garib placements
self.extra_garib_levels = [
["Atl1", 0],
["Atl2", 0],
["Atl3", 0],
["Atl?", 0],
["Crn1", 0],
["Crn2", 0],
["Crn3", 0],
["Crn?", 0],
["Prt1", 0],
["Prt2", 0],
["Prt3", 0],
["Prt?", 0],
["Pht1", 0],
["Pht2", 0],
["Pht3", 0],
["Pht?", 0],
["FoF1", 0],
["FoF2", 0],
["FoF3", 0],
["FoF?", 0],
["Otw1", 0],
["Otw2", 0],
["Otw3", 0],
["Otw?", 0]
]
self.starting_ball : str = "Rubber Ball"
#Grab Mr. Tips for hints
self.tip_locations : Dict[str, int] = {}
#Speaking of hints
self.mr_hints = {}
self.chicken_hints = {}
#Fake item names
self.fake_item_names : list = static_trap_name_table()
#Create null items for the table
world_garib_table.update(construct_blank_world_garibs(self.world_prefixes, self.level_prefixes))
#Garibs are Filler
self.garibs_are_filler = False
#Filler Item Counts
self.filler_item_counts : dict[str, int] = {}
self.filler_percent_table : dict[str, float] = {}
#Universal Tracker data
self.visited_worlds = 0
super(GloverWorld, self).__init__(world, player)
def level_from_string(self, name : str) -> int:
if name[3:4] in self.level_prefixes:
return self.level_prefixes.index(name[3:4])
if name.startswith("Hubworld"):
return 0
if name.startswith("Castle Cave"):
return 1
if name.startswith("Training"):
return 2
return -1
def world_from_string(self, name : str) -> int:
if name[:3] in self.world_prefixes:
return self.world_prefixes.index(name[:3])
if name.startswith("Hubworld") or name.startswith("Castle Cave") or name.startswith("Training"):
return 6
return -1
def level_from_lua_string(self, lua_name: str) -> int:
split_index = lua_name.find("_", 3)
if lua_name[split_index:] in self.lua_suffixes:
return self.lua_suffixes.index(lua_name[split_index:]) + 1
return -1
def world_from_lua_string(self, lua_name: str) -> int:
split_index = lua_name.find("_", 3)
if lua_name[:split_index] in self.lua_prefixes:
world_index = self.lua_prefixes.index(lua_name[:split_index])
if world_index >= 6:
world_index = 5
return world_index
return -1
def set_highest_valid_checkpoints(self):
#Limit checkpoint options if everything must be accessable
if self.options.accessibility.value == 0 and self.options.checkpoint_checks != 1:
#Carnival 2 (Pre-Rollercoaster)
self.spawn_checkpoint[4] = 1
#Fear 3 (Post-Warp)
self.spawn_checkpoint[14] = 3
#Intended Locks
if self.options.difficulty_logic.value == 0:
#Prehistoric 1 (Icicles)
self.spawn_checkpoint[9] = 1
#Prehistoric 3 (Lava Platforms)
self.spawn_checkpoint[11] = 3
#Without Switch Items
if not self.options.switches_checks:
#Intended Locks
if self.options.difficulty_logic.value == 0:
#Carnival 3 (Hands)
self.spawn_checkpoint[5] = 3
#Pirates 1 (Raise Ship)
self.spawn_checkpoint[6] = 3
#Prehistoric 2 (Lava Platforms)
self.spawn_checkpoint[10] = 2
#Fear 1 (Ball Gate)
self.spawn_checkpoint[12] = 2
#Space 3 (Glass Gate)
self.spawn_checkpoint[17] = 3
#Easy/Intended Locks
if self.options.difficulty_logic.value <= 1:
#Prehistoric 3 (Lava Platforms)
self.spawn_checkpoint[11] = 2
#Fear 2 (Lever Room)
self.spawn_checkpoint[13] = 2
def validate_options(self):
#Level Name input validation
for each_level, each_door in self.options.entrance_overrides.items():
if not self.valid_override_level_name(each_level):
raise OptionError("\""+ each_level + "\" is not a valid level for Entrance Overrides!")
if not self.valid_override_level_name(each_door):
raise OptionError("\""+ each_door + "\" is not a valid door for Entrance Overrides!")
for each_level in list(self.options.garib_order_overrides.keys()):
if not self.valid_override_level_name(each_level, False):
raise OptionError("\""+ each_level + "\" is not a valid level for Garib Order Overrides!")
garib_override_positions = list(self.options.garib_order_overrides.values())
entrance_override_doors = list(self.options.entrance_overrides.values())
#No duplicate override choices for Garib Order or World Order
if len(garib_override_positions) != len(set(garib_override_positions)):
raise OptionError("Two garib overrides choose the same position! Make sure all values are unique.")
if len(entrance_override_doors) != len(set(entrance_override_doors)):
raise OptionError("Two entrance overrides choose the same door! Make sure all values are unique.")
#All the scores are actual scores
for each_score in self.options.total_scores.value:
if each_score != 'None':
if not each_score.isdigit():
raise OptionError("\""+ each_score + "\" is not a valid score!")
each_score_int = int(each_score)
if each_score_int % 10000 != 0:
raise OptionError("Score \""+ each_score + "\" is not a multiple of 10000!")
if each_score_int < 10000:
raise OptionError("Score \""+ each_score + "\" is too low!")
if each_score_int >= 100000000:
raise OptionError("Score \""+ each_score + "\" is too high!")
for each_level in self.options.level_scores.value:
level_index = self.level_from_string(each_level)
if self.world_from_string(each_level) != 5 and level_index == 4:
raise OptionError("The only boss that gives enough score is Out of This World! Update your level scores.")
elif level_index == 0:
raise OptionError("Wayrooms do not have score!")
#Checkpoint Overrides In-Bounds
for target_level, set_checkpoint in self.options.checkpoint_overrides.items():
if not self.valid_override_level_name(target_level, False, False):
raise OptionError("\""+ target_level + "\" is not a valid level for Checkpoint Overrides!")
world_index = self.world_from_string(target_level)
level_index = self.level_from_string(target_level) - 1
max_checkpoint_value = self.spawn_checkpoint[(world_index * 3) + level_index]
if max_checkpoint_value < set_checkpoint or set_checkpoint < 1:
raise OptionError("Level " + target_level + " does not have a \"Checkpoint " + str(set_checkpoint) + "\"! Check your Checkpoint Overrides.")
#Level garibs shouldn't show up
if self.options.garib_logic == GaribLogic.option_level_garibs and self.options.filler_extra_garibs_weight.value > 0:
raise OptionError("Extra garibs cannot show up while garib logic is by level! Set your Filler Extra Garibs to 0.")
if self.options.victory_condition.value == VictoryCondition.option_golden_garibs and not self.using_ut:
#Skip this validation when using Universal Tracker because it can incorrectly lower the required Golden Garib count
self.validate_golden_garibs()
def validate_golden_garibs(self):
#Golden Garibs Goal Reachable
total_golden_garibs = self.options.golden_garib_count.value
total_golden_garibs += self.get_pre_fill_items().count("Golden Garib")
if total_golden_garibs < self.options.required_golden_garibs.value:
logging.warning("WARNING: Cannot require more golden garibs than there are in your world! Reducing your total to match.")
self.options.required_golden_garibs.value = total_golden_garibs
#Minimal?
if self.options.accessibility.value == Accessibility.option_minimal:
raise OptionError("Golden Garibs with Minimal Logic does not build solvable paths. Change one of those settings.")
#Golden Garibs Enough Filler
move_count = 27
#Jump Item in Pool
if not self.options.randomize_jump:
move_count -= 1
#Power Ball Item in Pool
if not (self.options.include_power_ball or self.options.starting_ball.value == 4):
move_count -= 1
#Filler items that always exist
filler_count = 0
#Lives
filler_count += 71
#Potions
filler_count += 24
#Cheat Chicken
filler_count += 1
#Ball Returns
filler_count += 7
if self.options.bonus_levels:
#Lives
filler_count += 18
#Potions
filler_count += 9
#Portalsanity
if self.options.portalsanity and not self.options.open_levels:
move_count += 29
else:
#Goals
filler_count += 31
#Settings that create filler items
if self.options.mr_tip_checks:
filler_count += 35
if self.options.enemysanity:
filler_count += 116
#Pirates Bonus
if self.options.bonus_levels:
filler_count += 1
if self.options.insectity:
filler_count += 8
#Atlantis Bonus
if self.options.bonus_levels:
filler_count += 3
#At least 1 filler item for every type
move_count += 15
#Check if there's enough
if filler_count - (move_count + total_golden_garibs) < 0:
raise OptionError("There aren't enough filler items! (" + str(move_count + total_golden_garibs) + " core items and " + str(filler_count) + " filler). Lower your golden garib count or add more filler locations!")
def valid_override_level_name(self, in_level : str, allow_bosses : bool = True, allow_bonuses : bool = True) -> bool:
end_options = ["1", "2", "3"]
if self.options.bonus_levels and allow_bonuses:
end_options.append("?")
if allow_bosses:
end_options.append("!")
return in_level.endswith(tuple(end_options)) and in_level.startswith(tuple(["Atl", "Crn", "Prt", "Pht", "FoF", "Otw"])) and len(in_level) == 4
def percents_sum_100(self, percents_dict : dict) -> dict:
sum : float = 0
for all_percentages in percents_dict.values():
sum += all_percentages
if math.isclose(sum, 1):
return percents_dict
if math.isclose(sum, 0):
raise OptionError("All weight entries are 0! Check your Filler or Trap weights!")
for all_entries, all_percentages in percents_dict.items():
percents_dict[all_entries] /= sum
return percents_dict
def setup_entrance_randomization(self):
#While only certain levels exist, randomize only those.
randomizable_existing_levels = self.existing_levels.copy()
randomizable_existing_levels.remove("Training")
shuffled_existing_levels = randomizable_existing_levels.copy()
self.random.shuffle(shuffled_existing_levels)
originalEntries = self.wayroom_entrances.copy()
for level_index, level_name in enumerate(randomizable_existing_levels):
replaced_index = originalEntries.index(level_name)
self.wayroom_entrances[replaced_index] = shuffled_existing_levels[level_index]
#Remove bonus levels by placing them in vanilla spots
if not self.options.bonus_levels:
self.wayroom_entrances.remove("Atl?")
self.wayroom_entrances.remove("Crn?")
self.wayroom_entrances.remove("Prt?")
self.wayroom_entrances.remove("Pht?")
self.wayroom_entrances.remove("FoF?")
self.wayroom_entrances.remove("Otw?")
self.wayroom_entrances.insert(4, "Atl?")
self.wayroom_entrances.insert(9, "Crn?")
self.wayroom_entrances.insert(14, "Prt?")
self.wayroom_entrances.insert(19, "Pht?")
self.wayroom_entrances.insert(24, "FoF?")
self.wayroom_entrances.insert(29, "Otw?")
#Override randomized entrances here
for each_entry, each_door in self.options.entrance_overrides.value.items():
index = (self.world_from_string(each_door) * 5) + self.level_from_string(each_door) - 1
original_world = self.wayroom_entrances[index]
original_index = self.wayroom_entrances.index(each_entry)
self.wayroom_entrances[index] = each_entry
self.wayroom_entrances[original_index] = original_world
#Get all possible starts
possible_starts : list[str] = []
for each_level in self.existing_levels:
is_restrictive : bool = False
match each_level:
case "Atl2":
match self.spawn_checkpoint[1]:
case 0:
is_restrictive = self.options.difficulty_logic.value == 0 and self.ra
case 1:
is_restrictive = False
if not is_restrictive:
possible_starts.append(each_level)
#If you can't possibly spawn here
if not self.wayroom_entrances[0] in possible_starts:
#No forcing restrictive starts
if "Atl1" in self.options.entrance_overrides.value.values() or len(self.options.entrance_overrides.value.values()) > 28:
raise OptionError("Cannot force first level, restrictive start!")
#Pick a non-forced possible spot
possible_swaps : list[str] = []
for each_possible in possible_starts:
if not each_possible in self.options.entrance_overrides.value:
possible_swaps.append(each_possible)
#Switch Atlantis 1's door with it
new_start = self.random.choice(possible_swaps)
swap_index = self.wayroom_entrances.index(new_start)
self.wayroom_entrances[swap_index] = self.wayroom_entrances[0]
self.wayroom_entrances[0] = new_start
def setup_garib_order(self):
#Random Garib Sorting Order
if self.options.garib_sorting == GaribSorting.option_random_order:
self.random.shuffle(self.garib_level_order)
#Override the garib order
for each_level, each_placement in self.options.garib_order_overrides.items():
level_in_slot = self.garib_level_order[each_placement]
original_placement = -1
original_level = []
#Swap the randomized position with the overwriten one
for original_index, each_original in enumerate(self.garib_level_order):
if each_original[0] == each_level:
original_placement = original_index
original_level = each_original
self.garib_level_order[original_placement] = level_in_slot
self.garib_level_order[each_placement] = original_level
#Randomized Entrances, Garibs in Order
elif self.options.garib_sorting == GaribSorting.option_in_order and self.options.entrance_randomizer:
new_garib_order : list[list] = []
for level_name in self.wayroom_entrances:
#Find the level with the same name
for each_entry in self.garib_level_order:
if each_entry[0] == level_name:
new_garib_order.append(each_entry)
self.garib_level_order = new_garib_order
#Bonus level garibs all go at the end if they're disabled
if not self.options.bonus_levels:
self.garib_level_order.remove(["Atl?", 25])
self.garib_level_order.remove(["Crn?", 20])
self.garib_level_order.remove(["Prt?", 50])
self.garib_level_order.remove(["Pht?", 60])
self.garib_level_order.remove(["FoF?", 56])
self.garib_level_order.remove(["Otw?", 50])
self.garib_level_order.append(["Atl?", 25])
self.garib_level_order.append(["Crn?", 20])
self.garib_level_order.append(["Prt?", 50])
self.garib_level_order.append(["Pht?", 60])
self.garib_level_order.append(["FoF?", 56])
self.garib_level_order.append(["Otw?", 50])
elif self.options.accessibility == Accessibility.option_full and not (self.options.portalsanity or self.options.open_levels):
#Bonus levels unlock stuff, ergo sorting order is important to stop lockouts.
#Open levels and portalsanity already stop star mark lockouts
match self.options.garib_sorting:
case GaribSorting.option_random_order:
final_garib_level_index = self.wayroom_entrances.index(self.garib_level_order[-1][0])
final_garib_door = final_garib_level_index % 5
#Make sure the final star mark is a bonus level, (or is excluded)
if final_garib_door != 4:
#Get all non-boss levels placed at bonus doors
nonboss_at_bonus : list[str] = []
for bonus_door_levels in range(4,30,5):
level_at_bonus = self.wayroom_entrances[bonus_door_levels]
if not level_at_bonus.endswith("!"):
nonboss_at_bonus.append(level_at_bonus)
#If any of the bonus levels have garibs, that has to be the final level
if len(nonboss_at_bonus) > 0:
#Make one at random the final level instead
to_swap_name = self.random.choice(nonboss_at_bonus)
original_level_entry = self.garib_level_order[-1]
swap_level_entry = next(garib_levels for garib_levels in self.garib_level_order if garib_levels[0] == to_swap_name)
to_swap_index = self.garib_level_order.index(swap_level_entry)
self.garib_level_order[-1] = swap_level_entry
self.garib_level_order[to_swap_index] = original_level_entry
def give_starting_ball(self):
match self.options.starting_ball:
case StartingBall.option_rubber_ball:
self.starting_ball = "Rubber Ball"
case StartingBall.option_bowling_ball:
self.starting_ball = "Bowling Ball"
case StartingBall.option_ball_bearing:
self.starting_ball = "Ball Bearing"
case StartingBall.option_crystal_ball:
self.starting_ball = "Crystal"
case StartingBall.option_power_ball:
self.starting_ball = "Power Ball"
self.multiworld.push_precollected(self.create_item(self.starting_ball))
def generate_early(self):
self.using_ut = False
if hasattr(self.multiworld, "re_gen_passthrough"):
if self.game in self.multiworld.re_gen_passthrough:
slot_data: dict[str, Any] = self.multiworld.re_gen_passthrough[self.game]
if slot_data["version"] != self.version:
err_string: str = f"Glover APWorld version mismatch. Multiworld generated with " \
f"{slot_data['version']}; local install using {self.version}"
raise VersionException(err_string)
self.overwrite_options(self.multiworld.re_gen_passthrough[self.game])
self.using_ut = True
self.found_entrances_datastorage_key = "Glover_{team}_{player}_visited_worlds"
#Set the valid spawning checkpoints
self.set_highest_valid_checkpoints()
#Check if garibs are filler or not
self.garibs_are_filler = (not self.options.portalsanity) and ((self.options.difficulty_logic.value == 0 and not self.options.bonus_levels) or (self.options.open_levels == True))
#Validate options
self.validate_options()
#Setup the Filler Table logic
self.setup_filler_table()
#Shuffle Checkpoints
self.checkpoint_randomization()
#Level entry randomization
if self.options.entrance_randomizer:
self.setup_entrance_randomization()
#Set the garib order if the settings allow it
self.setup_garib_order()
#Set the starting ball
self.give_starting_ball()
#Jump randomization is so easy it can just be done here
if not self.options.randomize_jump:
self.multiworld.push_precollected(self.create_item("Jump"))
#You can always grab
self.multiworld.push_precollected(self.create_item("Grab"))
#Create fake items
self.fake_item_names.extend(dynamic_trap_name_table(self))
#Create the Mr. Tip Table
self.mr_tip_table : list[str] = generate_tip_table(self)
def checkpoint_randomization(self):
#Setup the spawning checkpoints
if self.options.spawning_checkpoint_randomizer:
#Choose where you spawn from a list of options created
spawning_options : list[list[int]] = []
for each_index, each_item in enumerate(self.spawn_checkpoint):
listEntry : list[int] = list(range(1, each_item + 1))
spawning_options.append(listEntry)
#Choose at random
for each_index, each_item in enumerate(self.spawn_checkpoint):
self.spawn_checkpoint[each_index] = self.random.choice(spawning_options[each_index])
#Override Checkpoints
for each_map, checkpoint_number in self.options.checkpoint_overrides.items():
checkpoint_entry = (self.world_from_string(each_map) * 3) + self.level_from_string(each_map) - 1
self.spawn_checkpoint[checkpoint_entry] = checkpoint_number
else:
#By default, they're all Checkpoint 1
for each_item in range(len(self.spawn_checkpoint)):
self.spawn_checkpoint[each_item] = 1
def create_regions(self):
multiworld = self.multiworld
player = self.player
#Build all the game locations, and the locations contained under them
multiworld.regions.append(Region("Menu", player, multiworld))
build_data(self)
#Randomize the entrances for those remaining regions
self.entrance_randomizer()
#Create the victory conditon
self.setup_victory()
#Create the rules for garibs now, so filler generation works correct
self.garib_item_rules()
self.create_total_scores()
#Total Scores
def create_total_scores(self):
score_locations : list[Location] = []
menu_region = self.get_region("Menu")
for each_score in self.options.total_scores.value:
if each_score == 'None':
continue
score_address = int(each_score) + 100000000
each_location = Location(self.player, each_score + " Score", score_address, menu_region)
menu_region.locations.append(each_location)
score_locations.append(each_location)
for level_index, each_level in enumerate(self.level_prefixes):
for world_index, each_world in enumerate(self.world_prefixes):
level_name = each_world + each_level
if (level_index == 4 and world_index != 5) or level_index == 0:
continue
for each_score_location in score_locations:
if world_index == 0 and level_index == 0:
set_rule(each_score_location, lambda state, plr = self.player, scl = level_name + ": Score": state.can_reach_location(scl, plr))
else:
add_rule(each_score_location, lambda state, plr = self.player, scl = level_name + ": Score": state.can_reach_location(scl, plr), "or")
def create_event(self, event : str) -> GloverItem:
return GloverItem(event, ItemClassification.progression, None, self.player)
def create_item(self, name) -> GloverItem:
item_classification = None
item_id = -1
#knownLevelAndWorld = [self.level_from_string(self, name), self.world_from_string(self, name)]
item_data = find_item_data(self, name)
item_id = item_data.glid
match item_data.type:
case "Proguseful":
item_classification = ItemClassification.progression | ItemClassification.useful
case "Progression":
item_classification = ItemClassification.progression
case "Useful":
item_classification = ItemClassification.useful
case "Filler":
item_classification = ItemClassification.filler
case "Trap":
item_classification = ItemClassification.trap
case "Garib":
#If the garibs have no use, they're filler
if self == None:
#ItemLink Fallback
item_classification = ItemClassification.progression_deprioritized_skip_balancing
elif self.garibs_are_filler:
item_classification = ItemClassification.filler
#If they're part of World Sorting logic, don't skip balancing for them
else:
item_classification = ItemClassification.progression_deprioritized_skip_balancing
case "Star":
#Star Marks are filler if you're on Intended
if self == None:
#ItemLink Fallback
item_classification = ItemClassification.progression
elif self.options.difficulty_logic.value == 0:
item_classification = ItemClassification.filler
else:
item_classification = ItemClassification.progression
name_for_use = name
#Garibsanity is just Garib
if name == "Garibsanity":
name_for_use = "Garib"
#Convert Extra Garibs into other types
#if name == "Extra Garibs":
# name_for_use = convert_extra_garibs(self)
item_output : GloverItem = GloverItem(name_for_use, item_classification, item_id, self.player)
#Rename traps on pedestals
if item_data.type == "Trap" and self != None:
fake_item_name = select_trap_item_name(self, name_for_use)
item_output.fake_name = fake_item_name
return item_output
def percent_of(self, percent : int) -> float:
return (float(percent) / 100.0)
def create_items(self) -> None:
#Garib Logic
garib_items = []
match self.options.garib_logic:
#0: Level Garibs (No items to be sent)
#Garib Groups
case GaribLogic.option_garib_groups:
if self.options.garib_sorting == GaribSorting.option_by_level:
garib_items = list(world_garib_table.keys())
if not self.options.bonus_levels:
garib_items = list(filter(lambda a: a[3:4] != "?", garib_items))
else:
garib_items = list(decoupled_garib_table.keys())
#Individual Garibs
case GaribLogic.option_garibsanity:
if self.options.garib_sorting == GaribSorting.option_by_level:
garib_items = list(garibsanity_world_table.keys())
if not self.options.bonus_levels:
garib_items = list(filter(lambda a: a[3:4] != "?", garib_items))
else:
garib_items = ["Garibsanity"]
#Checkpoint Logic
checkpoint_items = []
if self.options.checkpoint_checks.value:
for each_checkpoint in checkpoint_table:
level_offset = self.level_from_string(each_checkpoint)
world_offset = self.world_from_string(each_checkpoint)
if self.spawn_checkpoint[(level_offset - 1) + (world_offset * 3)] != int(each_checkpoint[-1]):
checkpoint_items.append(each_checkpoint)
#Level Event Logic
event_items = []
if self.options.switches_checks.value == 1:
event_items = list(level_event_table.keys())
#Portalsanity Items only have worth for non-open levels gameplay
portalsanity_items = []
if self.options.portalsanity and not self.options.open_levels:
portalsanity_items = list(portalsanity_table.keys())
#Moves
move_items = list(move_table.keys())
if not self.options.include_power_ball and self.starting_ball != "Power Ball":
move_items.remove("Power Ball")
if not self.options.randomize_jump:
move_items.remove("Jump")
move_items.remove("Grab")
#You don't need the item pool to contain your starting ball
move_items.remove(self.starting_ball)
#Golden Garibs
if self.options.victory_condition.value == 2:
move_items.append("Golden Garib")
#Potions
potion_items = list(potion_table.keys())
#Apply all core items
all_core_items = []
all_core_items.extend(move_items)
all_core_items.extend(checkpoint_items)
all_core_items.extend(portalsanity_items)
all_core_items.extend(event_items)
all_core_items.extend(potion_items)
#Filler garibs can be disabled here
if (not self.garibs_are_filler) or not self.options.disable_garib_items:
all_core_items.extend(garib_items)
core_item_count = 0
#Core Items
for each_item in all_core_items:
for _ in range(find_item_data(self, each_item).qty):
self.multiworld.itempool.append(self.create_item(each_item))
core_item_count += 1
#Calculate the total number of filler items needed to fill the missing locations
total_locations : int = len(self.multiworld.get_unfilled_locations(self.player))
total_core_items : int = core_item_count + len(self.get_pre_fill_items())
total_filler_items : int = int(total_locations - total_core_items)
#Create a filler item for each missing location
for _ in range(total_filler_items):
self.multiworld.itempool.append(self.create_filler())
#Constructs the table for refrence using the get_filler_item_name function
def setup_filler_table(self):
filler_percentages : dict[str, float] = {
"Extra Garibs" : self.percent_of(self.options.filler_extra_garibs_weight.value),
"Chicken Sound" : self.percent_of(self.options.filler_chicken_sound_weight.value),
"Life" : self.percent_of(self.options.filler_life_weight.value),
"Boomerang Spell" : self.percent_of(self.options.filler_boomerang_weight.value),
"Beachball Spell" : self.percent_of(self.options.filler_beachball_weight.value),
"Hercules Spell" : self.percent_of(self.options.filler_hercules_weight.value),
"Helicopter Spell" : self.percent_of(self.options.filler_helicopter_weight.value),
"Speed Spell" : self.percent_of(self.options.filler_speed_weight.value),
"Frog Spell" : self.percent_of(self.options.filler_frog_weight.value),
"Death Spell" : self.percent_of(self.options.filler_death_weight.value),
"Sticky Spell" : self.percent_of(self.options.filler_sticky_weight.value),
"Big Ball" : self.percent_of(self.options.filler_big_ball_weight.value),
"Low Gravity" : self.percent_of(self.options.filler_low_gravity_weight.value)
}
trap_percentages : dict[str, float] = {
"Frog Trap" : self.percent_of(self.options.frog_trap_weight.value),
"Cursed Ball Trap" : self.percent_of(self.options.cursed_ball_trap_weight.value),
"Instant Crystal Trap" : self.percent_of(self.options.instant_crystal_trap_weight.value),
"Camera Rotate Trap" : self.percent_of(self.options.camera_rotate_trap_weight.value),
"Fish Eye Trap" : self.percent_of(self.options.fish_eye_trap_weight.value),
"Enemy Ball Trap" : self.percent_of(self.options.enemy_ball_trap_weight.value),
"Control Ball Trap" : self.percent_of(self.options.control_ball_trap_weight.value),
"Invisiball Trap" : self.percent_of(self.options.invisiball_trap_weight.value),
"Tip Trap" : self.percent_of(self.options.tip_trap_weight.value)
}
trap_percentage = self.percent_of(self.options.trap_percentage.value)
filler_percentage = 1 - trap_percentage
#Weighted off the sum assuming there are entries, multiplied by the percentage of each type
if filler_percentage != 0:
filler_percentages = self.percents_sum_100(filler_percentages)
for each_filler in filler_percentages:
filler_percentages[each_filler] *= filler_percentage
if trap_percentage != 0:
trap_percentages = self.percents_sum_100(trap_percentages)
for each_trap in trap_percentages:
trap_percentages[each_trap] *= trap_percentage
#Create the percentage table for calculation
self.filler_percent_table = {**filler_percentages, **trap_percentages}
#Trim 0 entries
self.filler_percent_table = {key:val for key, val in self.filler_percent_table.items() if val > 0}
#Create the counter of filler items for comparision
for each_entry in self.filler_percent_table:
self.filler_item_counts[each_entry] = 0
self.total_filler : float = 0.0
def get_filler_item_name(self):
output : str
#Make sure there's 1 of every weighted item at minimum
one_minimum = {key:val for key, val in self.filler_item_counts.items() if val == 0}
if len(one_minimum) > 0:
output = self.random.choice(list(one_minimum.keys()))
else:
#Create a table to see how far you are from the target percentage
percentage_offsets : dict[str, float] = {}
for each_item in self.filler_item_counts:
#First, get the current actual item percentage
each_percent = self.filler_item_counts[each_item] / self.total_filler
#The offset is the target percentage minus the current percentage
percentage_offsets[each_item] = self.filler_percent_table[each_item] - each_percent
if len(percentage_offsets) > 0:
output = max(percentage_offsets, key=percentage_offsets.get)
else:
#Item Link falls back to lives.
return "Life"
#Return the next expected item
self.filler_item_counts[output] += 1
self.total_filler += 1
return output
def next_garib_level(self) -> str:
lowest_extra_garibs : int = 999
levels_from_lowest_extras : list = []
#Get the levels with the least extra garibs
for each_garib_level in self.extra_garib_levels:
#If it's the same number as the lowest extra garibs, append it
if each_garib_level[1] == lowest_extra_garibs:
levels_from_lowest_extras.append(each_garib_level)
#If it's lower, it's the new standard
elif each_garib_level[1] < lowest_extra_garibs:
lowest_extra_garibs = each_garib_level[1]
levels_from_lowest_extras = [each_garib_level]
#Going with garibs of this name
lowest_garib_count : int = 999
levels_from_lowest_count : list = []
for each_lowest_levels in levels_from_lowest_extras:
for each_entry in self.garib_level_order:
#Get the coresponding level order entry
if each_entry[0] == each_lowest_levels[0]:
#Get the levels with the least garibs
if each_entry[1] < lowest_garib_count:
lowest_garib_count = each_entry[1]
levels_from_lowest_count = [each_lowest_levels]
elif each_entry[1] == lowest_garib_count:
levels_from_lowest_count.append(each_lowest_levels)
#Now finally, from the levels with the least garibs and the least extra garib items, pick one at random
chosen_level = self.random.choice(levels_from_lowest_count)
#Get the index of it
chosen_index : int = self.extra_garib_levels.index(chosen_level)
#Note that you got a garib there, and return the name for use
self.extra_garib_levels[chosen_index][1] += 1
return self.extra_garib_levels[chosen_index][0]
def garib_item_rules(self):
#Garib items now combine with the garib sorting type and garib rules to create rules
player : int = self.player
#Unless you've set it to level garibs, in which case it's so straight forward it's done in JsonReader
if self.options.garib_logic == GaribLogic.option_level_garibs:
return
#Filler Garibs
if self.garibs_are_filler:
for each_level in self.garib_level_order:
#Ignore bonus levels if it's disabled
if each_level[0].endswith("?"):
continue
#At the next level
level_all_garibs : Location = self.multiworld.get_location(each_level[0] + ": All Garibs", player)
set_rule(level_all_garibs, lambda state: True)
#Otherwise, start by the sorting method, since it has the most major effect on how garib rules act
elif self.options.garib_sorting == GaribSorting.option_by_level:
#Garibs are sent to specific levels
garib_level_suffixes = ["1", "2", "3", "?"]
if not self.options.bonus_levels:
garib_level_suffixes.remove("?")
#The table for use
table_for_use : dict
match self.options.garib_logic:
case GaribLogic.option_garib_groups:
table_for_use = world_garib_table
case GaribLogic.option_garibsanity:
table_for_use = garibsanity_world_table
#Go over all relevant levels
for world_prefix in self.world_prefixes:
for garib_level_suffix in garib_level_suffixes:
world_name = world_prefix + garib_level_suffix
#Get the "All Garibs" location to set rules for
level_all_garibs : Location = self.multiworld.get_location(world_name + ": All Garibs", player)
#Get all garibs groups that belong to the given world
garib_item_names : list[str] = []
garib_item_count : int = 0
for each_key, each_item in table_for_use.items():
if each_item.qty <= 0:
continue
if each_key.startswith(world_name):
garib_item_names.append(each_key)
garib_item_count += each_item.qty
#With the total number of garib items you need
if len(garib_item_names) > 0:
set_rule(level_all_garibs, lambda state, required_groups = garib_item_names, required_group_count = garib_item_count: state.has_from_list(required_groups, player, required_group_count))
else:
#If they're decoupled from levels, count directly
total_required_garibs : int = 0
#Garibs are collected in the given order
for each_level in self.garib_level_order:
#Ignore bonus levels if it's disabled
if each_level[0].endswith("?") and not self.options.bonus_levels:
continue
#At the next level
level_all_garibs : Location = self.multiworld.get_location(each_level[0] + ": All Garibs", player)
#Require the cumulative garib count of all garib completions before this one
total_required_garibs += each_level[1]
set_rule(level_all_garibs, lambda state, cumulative_garib_requirement = total_required_garibs: state.has("Total Garibs", player, cumulative_garib_requirement))
def entrance_randomizer(self):
entry_name : list[str] = ["1", "2", "3", "Boss", "Bonus"]
multiworld : MultiWorld = self.multiworld
player : int = self.player
#Menu loads into the hubworld
hubworld : Region = multiworld.get_region("Hubworld", player)
multiworld.get_region("Menu", player).connect(hubworld)
hubworld.connect(multiworld.get_region("Hubworld: Main W/Ball", player))
castle_cave : Region = multiworld.get_region("Castle Cave", player)
hubworld.connect(castle_cave)
castle_cave.connect(multiworld.get_region("Castle Cave: Main W/Ball", player))
#Apply wayroom entrances
for world_index, each_world_prefix in enumerate(self.world_prefixes):
world_offset : int = world_index * 5
wayroom_name : str = each_world_prefix + "H"
hubroom : Region = multiworld.get_region(wayroom_name, player)
hubroom.connect(multiworld.get_region(wayroom_name + ": Main W/Ball", player))
#Entries
for entry_index, each_entry_suffix in enumerate(entry_name):
#Connect hubs to the right location
offset : int = world_offset + entry_index
location_name : str = wayroom_name + ": Entry " + each_entry_suffix
connecting_level_name : str = self.wayroom_entrances[offset]
if connecting_level_name.endswith('?') and not self.options.bonus_levels:
continue
entry_region : Region = multiworld.get_location(location_name, player).parent_region
if self.using_ut and self.options.entrance_randomizer and self.multiworld.enforce_deferred_connections != "off":
hub_exit: Entrance = entry_region.create_exit(location_name)
set_rule(hub_exit, lambda state, each_location = location_name: state.can_reach_location(each_location, player))
else:
connecting_level : Region = multiworld.get_region(connecting_level_name, player)
entry_region.connect(connecting_level, location_name, lambda state, each_location = location_name: state.can_reach_location(each_location, player))
#Default portal and star positions
if not self.options.portalsanity:
self.populate_goals_and_marks(connecting_level_name, wayroom_name, entry_index)
else:
#Garibsanity only manually places the second 'boss clear' location
self.portalsanity_plugs(connecting_level_name, wayroom_name, entry_index)
#Getting all garibs from non-boss levels opens the last gate
if self.options.bonus_levels and not self.options.portalsanity:
bonus_unlock : Location
#Makes bonus unlocks happen as vanilla
bonus_unlock = Location(player, wayroom_name + ": Three Stars", None, hubroom)
bonus_unlock.place_locked_item(self.create_event(wayroom_name + " Bonus Gate"))
hubroom.locations.append(bonus_unlock)
star_names : list[str] = []
#Requires your 1, 2 and 3 stars
for each_star_number in range(1, 4):
star_name = wayroom_name + " " + str(each_star_number) + " Star"
star_names.append(star_name)
#Require the three stars
if not self.options.open_levels:
set_rule(bonus_unlock, lambda state, required_stars = star_names: state.has_all(required_stars, player))
#Entry Names
hub_entry_names : list[str] = [
"Atlantis Hub Entry",
"Carnival Hub Entry",
"Pirates Hub Entry",
"Prehistoric Hub Entry",
"Fear Hub Entry",
"OotW Hub Entry",
"Well Entry"
]
hub_gates : list[str] = [
"Hubworld Atlantis Gate",
"Hubworld Carnival Gate",
"Hubworld Pirate's Cove Gate",
"Hubworld Prehistoric Gate",
"Hubworld Fortress of Fear Gate",
"Hubworld Out of This World Gate"
]
final_location : Location = self.returning_crystal(castle_cave, 7, False, "E")
final_location.place_locked_item(self.create_event("Endscreen"))
#Apply hubworld entrances
for entrance_index, entrance_name in enumerate(self.overworld_entrances):
loading_zone : str = hub_entry_names[entrance_index]
connecting_name : str = entrance_name
if entrance_name == "Well":
connecting_name = self.wayroom_entrances[len(self.wayroom_entrances) - 1]
#Plug the well's completions up so they do nothing
if not self.options.portalsanity:
self.populate_goals_and_marks(connecting_name, "Well", -1)
else:
self.portalsanity_plugs(connecting_name, "Well", -1)
connecting_level : Region = multiworld.get_region(connecting_name, player)
reaching_location : str = "Hubworld: " + loading_zone
reaching_region : Region = multiworld.get_location(reaching_location, player).parent_region
reaching_region.connect(connecting_level, loading_zone, lambda state, each_location = reaching_location: state.can_reach_location(each_location, player))
#Crystal turn-ins logic
unlocking_crystal_location : Location | None = None
#Place gate openings
match entrance_index:
case 0:
#Requires 1/7 Balls Returned
unlocking_crystal_location = self.returning_crystal(castle_cave, 1, True, "A")
case 1:
#Requires 2/7 Balls Returned
unlocking_crystal_location = self.returning_crystal(castle_cave, 2, True, "A")
case 2:
#Requires 2/7 Balls Returned
unlocking_crystal_location = self.returning_crystal(castle_cave, 2, True, "B")
case 3:
#Requires 4/7 Balls Returned
unlocking_crystal_location = self.returning_crystal(castle_cave, 4, True, "A")
case 4:
#Requires 4/7 Balls Returned
unlocking_crystal_location = self.returning_crystal(castle_cave, 4, True, "B")
case 5:
#Requires 6/7 Balls Returned
unlocking_crystal_location = self.returning_crystal(castle_cave, 6, True, "A")
#Put the gate to it at the crystal location
if unlocking_crystal_location != None:
unlocking_crystal_location.place_locked_item(self.create_event(hub_gates[entrance_index]))
#Crystal unlock locations (To reduce restrictive starts)
for each_crystal in range(1, 8):
self.returning_crystal(castle_cave, each_crystal, False, "", 1945 + each_crystal)
def extend_hint_information(self, hint_data : Dict[int, Dict[int, str]]):
player = self.player
if not self.options.entrance_randomizer:
return
hint_data[player] = {}
#Go over the randomizable regions
for vanilla_index, level_region_name in enumerate(self.existing_levels):
#If it's in a vanilla position, don't flag the location as unique
randomized_index = self.wayroom_entrances.index(level_region_name)
if vanilla_index == randomized_index:
continue
entrance_name : str = self.existing_levels[randomized_index]
#Get the current level region
level_region : Region = self.get_region(level_region_name)
for each_region in self.recursive_region_search([level_region]):
for each_location in each_region.locations:
each_address = each_location.address
#Don't consider event locations, since they aren't hinted
if each_address == None:
continue
hint_data[player][each_address] = self.verbose_level_name(entrance_name)
#Verbose
def verbose_level_name(self, inLevel : str) -> str:
level_suffix : str = inLevel[3:4]
if level_suffix == "?":
level_suffix = "Bonus"
elif level_suffix == "!":
level_suffix = "Boss"
match self.world_from_string(inLevel):
case 0:
return "Atlantis " + level_suffix
case 1:
return "Carnival " + level_suffix
case 2:
return "Pirates Cove " + level_suffix
case 3:
return "Prehistoric " + level_suffix
case 4:
return "Fortress of Fear " + level_suffix
case 5:
return "Out of This World " + level_suffix
return "Tutorial Well"
#Get all regions contected to the exits of this region
def recursive_region_search(self, known_regions : list[Region]) -> list[Region]:
for each_region in known_regions:
for each_exit in each_region.exits:
#Stop Two-Way refrences from looping forever
if not each_exit.connected_region in known_regions:
known_regions.append(each_exit.connected_region)
known_regions = self.recursive_region_search(known_regions)
return known_regions
#Puts the victory location
def setup_victory(self):
multiworld = self.multiworld
player = self.player
victory_condition = "ERROR"
match self.options.victory_condition.value:
case 1:
castle_cave : Region = multiworld.get_region("Castle Cave", player)
victory_condition = str(self.options.required_crystals.value) + " Balls Returned"
victory_location : Location = self.returning_crystal(castle_cave, self.options.required_crystals.value, False, "G")
victory_location.place_locked_item(self.create_event(victory_condition))
case 2:
menu : Region = multiworld.get_region("Menu", player)
victory_location : Location = Location(player, "Golden Garibs Victory", None, menu)
menu.locations.append(victory_location)
rggs_to_win = self.options.required_golden_garibs.value
victory_condition = str(rggs_to_win) + " Golden Garibs"
victory_location.place_locked_item(self.create_event(victory_condition))
set_rule(victory_location, lambda state, rgg = rggs_to_win: state.has("Golden Garib", player, rgg))
print(victory_condition)
case _:
victory_condition = "Endscreen"
multiworld.completion_condition[player] = lambda state: state.has(victory_condition, player)
#Crystal return locations
def returning_crystal(self, castle_cave : Region, required_balls : int, can_be_open : bool, suffix : str = "", apId : int | None = None) -> Location:
player = self.player
crystal_return_location : Location = Location(player, "Ball Turn-In " + str(required_balls) + suffix, apId, castle_cave)
castle_cave.locations.append(crystal_return_location)
#Open Worlds Glover should bypass the need for unlocking hubs logic-wise
if not self.options.open_worlds or not can_be_open:
set_rule(crystal_return_location, lambda state, returned_balls_needed = required_balls - 1: state.has("Returned Balls", player, returned_balls_needed))
return crystal_return_location
#Portalsanity Gates and Marks
def portalsanity_plugs(self, connecting_level_name : str, wayroom_name : str, entry_index : int):
player = self.player
#Disable bonus levels
if connecting_level_name.endswith('?') and not self.options.bonus_levels:
return
#Completions
if not connecting_level_name.endswith(('1','2','3','?')):
garibs_location = self.multiworld.get_location(connecting_level_name + ": Completion", player)
garibs_location.place_locked_item(self.create_event(wayroom_name + " Completed"))
#The ball goes at the 4th gate
if entry_index == 3:
goal_or_boss : str = ": Goal"
if connecting_level_name.endswith('!'):
goal_or_boss = ": Boss"
goal_item = self.create_event(wayroom_name + " Ball")
goal_location : Location = self.multiworld.get_location(connecting_level_name + goal_or_boss, player)
psudo_goal_location : Location = Location(player, connecting_level_name + goal_or_boss + " Reached", None, goal_location.parent_region)
goal_location.parent_region.locations.append(psudo_goal_location)
set_rule(psudo_goal_location, lambda state, psudo_goal = goal_location.name: state.can_reach_location(psudo_goal, player))
psudo_goal_location.place_locked_item(goal_item)
elif self.options.open_levels and entry_index != -1:
#What fixed item is there?
match entry_index:
case 0:
goal_item = self.create_event(wayroom_name + " 2 Gate")
case 1:
goal_item = self.create_event(wayroom_name + " 3 Gate")
case 2:
goal_item = self.create_event(wayroom_name + " Boss Gate")
case 4:
goal_item = self.create_event(wayroom_name + " Bonus Gate")
hub_region : Region = self.multiworld.get_region(wayroom_name, self.player)
open_gate_spot : Location = Location(player, wayroom_name + " " + self.level_prefixes[entry_index + 1] + " Access", None, hub_region)
hub_region.locations.append(open_gate_spot)
open_gate_spot.place_locked_item(goal_item)
#Lacking Portalsanity Gates and Marks
def populate_goals_and_marks(self, connecting_level_name : str, wayroom_name : str, entry_index : int):
player = self.player
#Disable bonus levels
if connecting_level_name.endswith('?') and not self.options.bonus_levels:
return
#Map Generation
goal_item : Item
all_garibs_item : Item
#What fixed item is there?
match entry_index:
case -1:
goal_item = self.create_event(wayroom_name + " Finished")
all_garibs_item = self.create_event(wayroom_name + " Completed")
case 0:
goal_item = self.create_event(wayroom_name + " 2 Gate")
all_garibs_item = self.create_event(wayroom_name + " 1 Star")
case 1:
goal_item = self.create_event(wayroom_name + " 3 Gate")
all_garibs_item = self.create_event(wayroom_name + " 2 Star")
case 2:
goal_item = self.create_event(wayroom_name + " Boss Gate")
all_garibs_item = self.create_event(wayroom_name + " 3 Star")
case 3:
goal_item = self.create_event(wayroom_name + " Ball")
all_garibs_item = self.create_event(wayroom_name + " Boss Star")
case 4:
goal_item = self.create_event(wayroom_name + " Bonus Complete")
all_garibs_item = self.create_event(wayroom_name + " Bonus Star")
#Tutorial, Level ! and ? Completions always have their goal event items
if (not self.options.open_levels) or entry_index >= 3 or entry_index == -1:
#What kind of level is this?
if connecting_level_name in self.existing_levels:
goal_or_boss : str = ": Goal"
if connecting_level_name.endswith('!'):
goal_or_boss = ": Boss"
goal_location : Location = self.multiworld.get_location(connecting_level_name + goal_or_boss, player)
psudo_goal_location : Location = Location(player, connecting_level_name + goal_or_boss + " Reached", None, goal_location.parent_region)
goal_location.parent_region.locations.append(psudo_goal_location)
set_rule(psudo_goal_location, lambda state, psudo_goal = goal_location.name: state.can_reach_location(psudo_goal, player))
psudo_goal_location.place_locked_item(goal_item)
#Open Levels give the gate event items for free
else:
hub_region : Region = self.multiworld.get_region(wayroom_name, self.player)
open_gate_spot : Location = Location(player, wayroom_name + " " + self.level_prefixes[entry_index + 1] + " Access", None, hub_region)
hub_region.locations.append(open_gate_spot)
open_gate_spot.place_locked_item(goal_item)
garibs_location : Location
#Levels with garibs
if connecting_level_name.endswith(('1','2','3','?')):
garibs_location = self.multiworld.get_location(connecting_level_name + ": All Garibs", player)
#Levels without garibs
else:
garibs_location = self.multiworld.get_location(connecting_level_name + ": Completion", player)
#Place them
garibs_location.place_locked_item(all_garibs_item)
def connect_entrances(self):
if self.options.generate_puml:
reachable_regions = self.multiworld.get_all_state().reachable_regions[self.player]
unreachable_regions = []
for each_region in self.multiworld.regions:
if not each_region in reachable_regions:
unreachable_regions.append(each_region)
visualize_regions(self.multiworld.get_region("Menu", self.player), "Glover.puml", regions_to_highlight=unreachable_regions)
return super().connect_entrances()
def build_options(self):
options: dict[str, Any] = self.options.as_dict(
"victory_condition",
"required_crystals",
"required_golden_garibs",
"difficulty_logic",
"death_link",
"tag_link",
"trap_link",
"starting_ball",
"garib_logic",
"garib_sorting",
"mad_garibs",
"random_garib_sounds",
"entrance_randomizer",
"portalsanity",
"open_worlds",
"open_levels",
"spawning_checkpoint_randomizer",
"bonus_levels",
"randomize_jump",
"include_power_ball",
"checkpoint_checks",
"switches_checks",
"mr_tip_checks",
"enemysanity",
"insectity",
"easy_ball_walk",
"mr_hints",
"mr_hints_scouts",
"mr_tip_text_display",
"chicken_hints",
"extra_garibs_value"
)
options["filler_duration"] = self.calculate_duration()
options["player_name"] = self.multiworld.player_name[self.player]
options["seed"] = self.random.randint(-6500000, 6500000)
options["version"] = self.version
return options
def calculate_duration(self):
return self.options.filler_duration.value - 1
def generate_hints(self):
self.mr_tip_text = {}
hint_groups = create_hints(self)
self.mr_hints = hint_groups[0]
self.chicken_hints = hint_groups[1]
#Mr. Tip Hint Display Text
if self.options.mr_tip_text_display.value == 1:
self.mr_tip_text = hint_groups[2]
if self.options.chicken_hints.value == 2:
self.vague_chicken_text = hint_groups[3]
else:
self.vague_chicken_text = {}
def generate_tip_text(self):
#Mr. Tip Custom Text
if self.options.mr_tip_text_display.value != 0:
for each_tip, tip_address in self.tip_locations.items():
if str(tip_address) in self.mr_tip_text:
continue
#Create unique tip text
self.mr_tip_text[str(tip_address)] = self.random.choice(self.mr_tip_table)
def slot_score_checks(self) -> dict[str, list[int]]:
slot_scores : dict[str, list[int]] = {}
for each_level, level_scores in self.options.level_scores.value.items():
ap_name = self.lua_world_name(each_level)
slot_scores[ap_name] = level_scores
if len(self.options.total_scores.value):
slot_scores["TOTAL"] = []
for each_score in self.options.total_scores.value:
if each_score == 'None':
continue
slot_scores["TOTAL"].append(int(each_score))
return slot_scores
def lua_world_name(self, original_name):
world_index : int = self.world_from_string(original_name)
level_index : int = self.level_from_string(original_name) - 1
if world_index >= 6:
level_index = 5
lua_prefix = self.lua_prefixes[world_index]
lua_suffix = self.lua_suffixes[level_index]
return lua_prefix + lua_suffix
def lua_decoupled_garib_order(self) -> dict[str, str]:
output = {}
for level_index, each_level in enumerate(self.garib_level_order):
lua_name = self.lua_world_name(each_level[0])
output[str(level_index)] = lua_name + "_GARIBS"
return output
def lua_world_entry_lookup_table(self) -> dict[str, str]:
output = {}
for each_wayroom_index, each_wayroom_entrance in enumerate(self.wayroom_entrances):
#Wayroom info
wayroom_orign = int(each_wayroom_index / 5) + 1
wayroom_door = (each_wayroom_index % 5) + 1
#Hub
if wayroom_orign >= 7:
wayroom_door = 0
#Table entry with original world as a key, and the data entries as
output[self.lua_world_name(each_wayroom_entrance)] = (wayroom_orign * 10) + wayroom_door
return output
def fill_hook(self, progitempool: list[Item], usefulitempool: list[Item], filleritempool: list[Item], fill_locations: list[Location]):
progitempool.sort(key = lambda item: item.player == self.player and self.is_garib_item(item.name))
def fill_slot_data(self) -> Dict[str, Any]:
options = self.build_options()
self.generate_hints()
self.generate_tip_text()
options["score_checks"] = self.slot_score_checks()
options["mr_hints_locations"] = self.mr_hints
options["mr_tips_text"] = self.mr_tip_text
options["chicken_hints_locations"] = self.chicken_hints
options["vague_chicken_text"] = self.vague_chicken_text
options["world_lookup"] = self.lua_world_entry_lookup_table()
options["garib_order"] = self.lua_decoupled_garib_order()
options["spawning_checkpoints"] = self.spawn_checkpoint
return options
def overwrite_options(self, slot_data: dict[str, Any]):
# Only bother with overwriting options that are relevant to logic
self.options.victory_condition.value = slot_data["victory_condition"]
self.options.required_crystals.value = slot_data["required_crystals"]
self.options.required_golden_garibs.value = slot_data["required_golden_garibs"]
self.options.difficulty_logic.value = slot_data["difficulty_logic"]
self.options.garib_logic.value = slot_data["garib_logic"]
self.options.garib_sorting.value = slot_data["garib_sorting"]
self.options.entrance_randomizer.value = slot_data["entrance_randomizer"]
self.options.portalsanity.value = slot_data["portalsanity"]
self.options.open_worlds.value = slot_data["open_worlds"]
self.options.open_levels.value = slot_data["open_levels"]
if "spawning_checkpoint_randomizer" in slot_data:
self.options.spawning_checkpoint_randomizer.value = slot_data["spawning_checkpoint_randomizer"]
else:
self.options.spawning_checkpoint_randomizer.value = slot_data["randomized_spawns"]
self.options.bonus_levels.value = slot_data["bonus_levels"]
self.options.checkpoint_checks.value = slot_data["checkpoint_checks"]
self.options.switches_checks.value = slot_data["switches_checks"]
self.options.mr_tip_checks.value = slot_data["mr_tip_checks"]
self.options.enemysanity.value = slot_data["enemysanity"]
self.options.insectity.value = slot_data["insectity"]
self.options.extra_garibs_value.value = slot_data["extra_garibs_value"]
# Don't generate a PUML during UT gen (unless debugging)
self.options.generate_puml.value = False
# Parse Score checks
self.parse_score_checks(slot_data["score_checks"])
# Create the overrides
self.parse_entrance_overrides(slot_data["world_lookup"])
self.parse_garib_overrides(slot_data["garib_order"])
self.parse_spawning_overrides(slot_data["spawning_checkpoints"])
def parse_score_checks(self, score_checks: dict[str, Any]):
level_scores: dict[str, int] = {}
for lua_level_name, score_value in score_checks.items():
if lua_level_name == "TOTAL":
self.options.total_scores.value = set([str(score) for score in score_value])
continue
world_index: int = self.world_from_lua_string(lua_level_name)
level_index: int = self.level_from_lua_string(lua_level_name)
override_key: str = f"{self.world_prefixes[world_index]}{self.level_prefixes[level_index]}"
level_scores[override_key] = score_value
self.options.level_scores.value = level_scores
def parse_entrance_overrides(self, world_lookup: dict[str, int]):
entrance_overrides: dict[str, str] = {}
for lua_level_name, entrance_position in world_lookup.items():
if lua_level_name == "AP_TRAINING_WORLD":
continue
key_world_index: int = self.world_from_lua_string(lua_level_name)
key_level_index: int = self.level_from_lua_string(lua_level_name)
override_key: str = f"{self.world_prefixes[key_world_index]}{self.level_prefixes[key_level_index]}"
value_world_index: int = int(entrance_position / 10) - 1
value_level_index: int = int(entrance_position % 10)
override_value: str = f"{self.world_prefixes[value_world_index]}{self.level_prefixes[value_level_index]}"
entrance_overrides[override_key] = override_value
self.options.entrance_overrides.value = entrance_overrides
def parse_garib_overrides(self, garib_order: dict[int, str]):
garib_order_overrides: dict[str, int] = {}
for garib_index, lua_level_garib_name in garib_order.items():
lua_level_name: str = lua_level_garib_name.removesuffix("_GARIBS")
world_index: int = self.world_from_lua_string(lua_level_name)
level_index: int = self.level_from_lua_string(lua_level_name)
override_key: str = f"{self.world_prefixes[world_index]}{self.level_prefixes[level_index]}"
garib_order_overrides[override_key] = int(garib_index)
self.options.garib_order_overrides.value = garib_order_overrides
def parse_spawning_overrides(self, spawning_checkpoints: list[int]):
checkpoint_overrides: dict[str, int] = {}
for index, spawning_checkpoint in enumerate(spawning_checkpoints):
world_index: int = int(index/3)
# we want indices 1, 2, and 3 for level index
level_index: int = (index % 3) + 1
override_key: str = f"{self.world_prefixes[world_index]}{self.level_prefixes[level_index]}"
checkpoint_overrides[override_key] = spawning_checkpoint
self.options.checkpoint_overrides.value = checkpoint_overrides
# Universal Tracker function; do not rename
def reconnect_found_entrances(self, key: str, value: Any) -> None:
def bit_iterator(bits):
while bits:
bit = bits & (~bits + 1)
yield int(math.log2(bit))
bits ^= bit
if value is None or key is None or self.multiworld.enforce_deferred_connections == "off":
return
if value | self.visited_worlds != self.visited_worlds:
new_bits: int = value & (~self.visited_worlds)
for bit_index in bit_iterator(new_bits):
world_name: str = self.existing_levels[bit_index]
hub_level_name: str = self.options.entrance_overrides.value[world_name]
hub_entrance_name: str = hub_level_name[:3] + "H: Entry " + hub_level_name[3:]
if hub_entrance_name.endswith("?"):
hub_entrance_name = hub_entrance_name.replace("?", "Bonus")
if hub_entrance_name.endswith("!"):
hub_entrance_name = hub_entrance_name.replace("!", "Boss")
hub_entrance: Entrance = self.get_entrance(hub_entrance_name)
connecting_world: Region = self.get_region(world_name)
hub_entrance.connect(connecting_world)
self.visited_worlds |= new_bits