🛈 This is the generator for version 0.8.5 of the apworld.
{
"game": "GZDoom (__WAD__)",
"version": 7,
"compatible_version":7,
"authors": ["ToxicFrog's robot army"]
}
import math
import sys
from dataclasses import dataclass
from importlib import resources
from worlds.AutoWorld import World, WebWorld
VERSION = resources.read_text(__package__, "VERSION").strip()
# print(f"{__package__}: UZArchipelago wad apworld version {VERSION}")
if 'worlds.uzdoom' not in sys.modules:
raise RuntimeError(f'Unable to load supporting libraries -- make sure that `uzdoom.apworld` is installed!')
uzdoom = sys.modules['worlds.uzdoom']
model = sys.modules['worlds.uzdoom.model']
included_logic = model.init_wads(__package__)
wad = included_logic.wad
from .Options import UZDoomOptions, UZDoomWeb
if uzdoom.VERSION != VERSION:
print(f"Warning: {__package__}: version mismatch between core apworld ({uzdoom.VERSION}) and wad apworld ({VERSION})")
class UZDoomWorld___WAD__(uzdoom.UZDoomWorld):
game = f"UZDoom ({wad.name})"
mod_version = VERSION
hidden = False
wad_logic = wad
options_dataclass = UZDoomOptions
options: UZDoomOptions
web: WebWorld = UZDoomWeb()
# Used by AP itself
item_name_to_id = model.unified_item_map(included_logic)
item_name_groups = model.unified_item_groups(included_logic)
location_name_to_id = model.unified_location_map(included_logic)
location_name_groups = model.unified_location_groups(included_logic)
# TODO: add/implement options for:
# - win logic: all levels/N levels/all bosses/N bosses
# - are bosses checks
# - are exits checks (in addition to giving you a clear flag)
# we do this by making the "level exit flag" an internal event at generation
# time, and at runtime just setting a flag on the Region object when the level
# is clear.
# - progressive automap -- automaps bind to the first unmapped level you have,
# and/or you need multiple automaps to display everything for a level (say,
# one gets you checks, one gets you geometry, one gets you progression hilights)
from dataclasses import dataclass
from math import ceil,floor
import sys
from Options import PerGameCommonOptions, Toggle, DeathLink, StartInventory, StartInventoryPool, OptionSet, NamedRange, Range, OptionDict, OptionList, OptionError, OptionGroup, Visibility
from worlds.AutoWorld import WebWorld
uzdoom = sys.modules['worlds.uzdoom']
model = sys.modules['worlds.uzdoom.model']
included_logic = model.init_wads(__package__)
wad = included_logic.wad
class MaxWeaponCopies(Range):
"""
Applies a limit to the number of copies of each weapon in the item pool.
Lower values mean you may have to wait longer before finding a given weapon,
but also means more checks will contain health, powerups, etc. rather than
duplicates of weapons you already have. A setting of 0 removes this limit
entirely.
This is an upper bound; it will not add more weapons to the pool than exist
in the WAD already. By default it is set to 1 per 8 levels.
"""
display_name = "Max weapon copies"
range_start = 0
range_end = len(wad.maps)
default = ceil(len(wad.maps)/8)
class StartingLevels(OptionSet):
__doc__ = """
Levels you can access at the start of the game. You will spawn with access
codes for these levels; if start_with_keys is enabled, you will also spawn
with all keys for them. The default setting tries to include enough levels
to generate reliably even in singleplayer with fairly restrictive settings.
If you are playing a multiworld game and want to start with nothing at all
(i.e. Doom is not playable until another world unlocks it), set this to [].
This option supports globbing expressions.""" + (
f"""
For solo play, to avoid generation failures due to a small sphere 1 in
this wad, you may need to either change included_item_categories to enable
more item categories, or add more maps here. If doing the latter, the
apworld's best guess at what to set this to is:
{wad.default_solo_starting_maps()}
"""
if wad.default_solo_starting_maps() else ""
)
display_name = "Starting levels"
default = sorted(map.map for map in wad.default_starting_maps())
if wad.get_flag('use_hub_logic'):
class StartWithKeys(Toggle):
"""Forced off because this WAD uses hub logic."""
default = False
visibility = Visibility.none
elif model.tuning_files(wad.package, wad.name):
class StartWithKeys(Toggle):
"""
If enabled, you will start with all the keys for your starting_levels.
"""
default = False
else:
class StartWithKeys(Toggle):
"""
If enabled, you will start with all the keys for your starting_levels.
This WAD lacks tuning data, so setting this to false may cause
generation failures, especially in singleplayer.
"""
default = True
class IncludedLevels(OptionSet):
"""
Levels to randomize. By default this is all levels in the wad.
"""
display_name = "Included levels"
default = sorted(wad.maps.keys())
class SpawnFilter(NamedRange):
"""
Tell the generator which spawn filter (information about what items and enemies
spawn in each map) the game will use.
You need to pick a setting here that matches the difficulty you will be playing
on. In unmodified games filters 1 through 5 correspond to the five difficulty
levels, ITYTD through Nightmare -- although in most WADs 1/2 and 4/5 use the
same spawns.
Some mods implement custom difficulty levels that use different spawn filters
than the original game, so make sure you choose a filter that matches the
difficulty you are actually playing. If you get it wrong, you will get a
warning on entering the game.
"""
display_name = "Spawn filter"
range_start = 1
range_end = 8
default = 3
special_range_names = {
"itytd": 1,
"hntr": 2,
"hmp": 3,
"uv": 4,
"nm": 5,
}
class IncludedItemCategories(OptionList):
"""
Which item categories to include in randomization. This controls both which
items are replaced with checks, and what the item pool contains.
Each entry has the format 'categories:percent'. You can use 'all' or 'none'
as aliases for '100' or '0'. See doc/glossary.md for an explanation of
what item categories are available. Categories can be combined using '-',
e.g. 'secret-health' means all health items that are in secrets.
For each location or item, entries are checked *in order* and the first entry
that matches is used. A category of '*' matches everything. Items or locations
that don't match any entry are excluded by default.
Using an item name, e.g. 'ArmorBonus:none', is also permitted, and will match
that exact item. To avoid ambiguity, you have to use Doom's internal name,
e.g. 'ClipBox' instead of 'Bullets' and 'ArtiHealth' instead of 'Quartz Flask'.
The default settings:
- exclude all items in secrets and the secrets themselves, as they can be
quite difficult to get for inexperienced players depending on the WAD;
- include all useful and progression items;
- exclude all small and medium items and all Heretic tools (that are neither
useful nor progression);
- and include everything else by default.
Turning on medium, small, or tool items can increase the number of checks by
10x or more, so be prepared for a grass-rando-like experience if you do that.
You can also use two special values in place of a percentage:
'vanilla'
All items in this category will be 'randomized' into their vanilla locations.
Use this instead of 'none' if you want to play with non-randomized keys or
weapons. They will still count towards hint cost calculations.
'shuffle'
Items in this category will be randomly shuffled with each other locally.
They will not count towards hint cost calculations. You cannot use this
on progression items.
'start'
All items in this category will be placed in your starting inventory instead
of in the item pool. Use 'key:start', 'weapon:start', or 'ap_map:start' to
start with all keys, weapons, or maps, respectively.
"""
display_name = "Included item/location categories"
default = [
'secret-sector:none', 'secret-marker:none', 'secret:none',
'ap_progression:all', 'ap_useful:all', 'big:all',
'medium:none', 'small:none', 'tool:none',
'*:all',
]
def verify(self, world, player_name, plando_options):
super(OptionList, self).verify(world, player_name, plando_options)
self.build_ratios()
def build_ratios(self):
self.ratios = {}
self.bucket_list = []
for config in self.actual_value():
key,ratio = config.split(':')
assert key != '' and ratio != '', 'Entries in included_item_categories must have both a category and a percentage'
if key in self.ratios:
raise OptionError(f'Duplicate entry {key} in included_item_categories')
self.ratios[key] = self.ratio_value(ratio)
self.bucket_list.append((frozenset(key.split('-')), key))
for key in ['ap_map']:
ratio = self.find_ratio(None, {key})
if ratio not in {1.0, 'start'}:
raise OptionError(f'Category {key} has invalid setting {ratio}; this category only permits "all" or "start".')
for key in ['key', 'weapon']:
ratio = self.find_ratio(None, {key})
if ratio not in {1.0, 'vanilla', 'start'}:
raise OptionError(f'Category {key} has invalid setting {ratio}; this category only permits "all", "vanilla", or "start".')
# Convenience field used later by the location access logic.
self.all_keys_are_vanilla = self.find_ratio(None, {'key'}) == 'vanilla'
def actual_value(self):
# Player is not allowed to override these parts. Level accesses must
# always be in the pool -- starting_levels will handle removing them
# if needed -- and we need a fallback for other flags if the user
# doesn't specify one.
return ['ap_level:all'] + self.value + ['ap_flag:all']
def ratio_value(self, string):
if string == 'all':
return 1.0
elif string == 'none':
return 0.0
elif string in {'vanilla', 'start', 'shuffle'}:
return string
else:
return int(string)/100.0
def ratio_for_bucket(self, bucket):
if not bucket:
return 0.0
return self.ratios[bucket]
def find_bucket(self, item_type, loc_cats):
for (categories,bucket) in self.bucket_list:
if bucket == '*' or categories == {item_type} or categories <= loc_cats:
return bucket
return None
def find_ratio(self, item_type, loc_cats):
return self.ratio_for_bucket(self.find_bucket(item_type, loc_cats))
def ratio_for_item(self, item):
return self.find_ratio(item.typename, item.categories)
def bucket_for_location(self, loc):
if loc.orig_item:
return self.find_bucket(loc.orig_item.typename, loc.categories)
else:
return self.find_bucket(None, loc.categories)
class LevelOrderBias(Range):
"""
How closely the randomizer tries to follow the original level order of the
WAD. Internally, this is the % of earlier levels you need to be able to beat
before it will consider later levels in logic. This is primarily useful for
enforcing at least a bit of difficulty progression rather than being dumped
straight from MAP01 to MAP30.
The default setting of 25% means it won't require you to go to (e.g.) MAP08
until after beating two of the preceding levels.
Starting levels are exempt from this check.
"""
display_name = "Level order bias"
range_start = 0
range_end = 100
default = 25 if not wad.use_hub_logic() else 0
visibility = Visibility.all if not wad.use_hub_logic() else Visibility.none
class LocalWeaponBias(Range):
"""
How much the randomizer cares about making sure you have access to the
weapons in a level before it considers that level to be in logic. Most Doom
levels are possible to beat from a pistol start by finding weapons in the
level itself, but in the randomizer there is no guarantee the level contains
any weapons at all; this setting makes the randomizer ensure that some of
the weapons you would normally find in the level are accessible before you
enter it.
The setting is a percentage; higher values mean the randomizer will try to
make more of the weapons accessible before you need to enter the level. At
100% it will not consider a level to be in logic until all of the weapons
normally found in it are accessible to you elsewhere.
Starting levels are exempt from this check.
"""
display_name = "In-level weapon bias"
range_start = 0
range_end = 100
default = 0
class GlobalWeaponBias(Range):
"""
How much the randomizer cares about making sure you have access to the weapons
you'd start a level with when not pistol-starting (i.e. weapons you'd carry
over from earlier levels). This setting is somewhat conservative in that it
doesn't take into account death exits.
The setting is a percentage; higher values mean the randomizer will try to
make more of the weapons accessible before you need to enter the level. At
100% it will not consider a level to be in logic until all of the weapons
normally found before entering it are availalble to you elsewhere.
Starting levels are exempt from this check.
"""
display_name = "Carryover weapon bias"
range_start = 0
range_end = 100
default = 50
class WinMapCount(Range):
"""
How many maps you need to clear to win the game. By default this is all maps
in the wad.
"""
display_name = "Number of maps to win"
range_start = 0
range_end = len(wad.all_winnable_map_names())
default = len(wad.all_winnable_map_names())
class WinMapNames(OptionSet):
"""
Which specific maps you need to clear to win the game (assuming you have
changed win_map_count so as not to require all maps). The default is
Archipelago's best guess at which maps are end-of-episode or end-of-game
levels.
"""
display_name = "Specific maps to win"
default = sorted([map for map in wad.all_boss_map_names()])
valid_keys = sorted([map for map in wad.all_winnable_map_names()])
class AllowRespawn(Toggle):
"""
If enabled, the player will respawn at the start of the level on death. If
disabled they must restore from an earlier save.
NOTE: restoring from a save will restore the state of the world, but NOT the
state of the randomizer -- checks already collected will remain collected and
items used from the randomizer inventory will remain used.
"""
display_name = "Allow Respawn"
default = True
class FullPersistence(Toggle):
"""
If enabled, all levels will be fully persistent, in the same manner as Hexen
hubclusters. Levels can be reset from the level select menu, but this is all
or nothing: there is no way to reset individual levels. This is generally
reliable, but a minority of wads or gameplay mods may break with it.
"""
display_name = "Persistent Levels"
default = True
class PreTuningMode(Toggle):
"""
This setting is for logic developers. If enabled, all other options are
overridden. All checks contain their original items (i.e. there is no
randomization) and you start with access to all levels, all automaps, and no
keys. Checks for all difficulties are spawned regardless of what difficulty
you play on.
The intent of this mode is to let you play through the game, or specific
levels, "normally", to generate a tuning file, even in cases where the
initial scan is so conservative as to cause generation failures.
Pretuning mode also disables persistent mode and disables automatic MAPINFO
generation, so the game will retain its original episode divisions,
intermission text, etc.
"""
display_name = "Pretuning Mode"
default = False
class UZDoomStartInventory(StartInventory):
"""
Start with the specified amount of these items. You can list any valid
inventory item from the WAD you are playing, even if the randomizer doesn't
know about it, e.g. "BFG9000: 1" will work even if the WAD contains no BFGs.
"""
local_actors: dict = None
def verify(self, world, player_name, plando_options):
self.local_actors = {
typename: count
for typename, count in self.value.items()
if typename not in world.item_names
}
self.value = {
typename: count
for typename, count in self.value.items()
if typename in world.item_names
}
super(StartInventory, self).verify(world, player_name, plando_options)
@dataclass
class UZDoomOptions(PerGameCommonOptions):
# Skill level, WAD, and level selection
spawn_filter: SpawnFilter
starting_levels: StartingLevels
start_with_keys: StartWithKeys
included_levels: IncludedLevels
# Win conditions
win_map_count: WinMapCount
win_map_names: WinMapNames
# Combat logic
level_order_bias: LevelOrderBias
local_weapon_bias: LocalWeaponBias
carryover_weapon_bias: GlobalWeaponBias
# Other settings
allow_respawn: AllowRespawn
full_persistence: FullPersistence
pretuning_mode: PreTuningMode
# Location/item pool control
max_weapon_copies: MaxWeaponCopies
included_item_categories: IncludedItemCategories
# Stock settings
start_inventory: UZDoomStartInventory
start_inventory_from_pool: StartInventoryPool
class UZDoomWeb(WebWorld):
game = f"UZDoom ({wad.name})"
option_groups = [
OptionGroup("Starting Conditions", [
SpawnFilter, StartingLevels, StartWithKeys, IncludedLevels, UZDoomStartInventory,
]),
OptionGroup("Win Conditions", [
WinMapCount, WinMapNames,
]),
OptionGroup("Combat Logic", [
LevelOrderBias, LocalWeaponBias, GlobalWeaponBias,
]),
OptionGroup("Gameplay Settings", [
AllowRespawn, FullPersistence,
]),
OptionGroup("Randomization Settings", [
PreTuningMode, MaxWeaponCopies, IncludedItemCategories,
]),
]
# Apply custom option adjustments, if the wad has a custom.py
# Use this by defining a custom_options function in custom.py that takes the
# UZDoomOptions type as the first argument and any options it wants to modify
# as trailing keyword arguments; it can then modify the class fields as it sees
# fit. See wads/the_adventures_of_square/custom.py for a simple example.
try:
from .custom import custom_options
custom_options(UZDoomOptions, **{
name: field.type for name, field in UZDoomOptions.__dataclass_fields__.items()
})
except ModuleNotFoundError:
# It's ok if there's no custom.py. Any other problem loading it is an error.
pass