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
1707 lines
83 KiB
Python
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
|