⚠️ This is the generator for the unstable version of the apworld (gzap-0.8.5-57-g0cfd153). Click here for latest stable (0.8.5).
{
"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, TextChoice, Choice
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
###############################################################################
# Initial conditions
###############################################################################
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 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"""
This wad has a small sphere 1 with default settings.. To avoid generation
failures in solo play, you may need to do one or more of:
- enable `secrets`;
- turn up `medium_items` and/or `small_items`;
- enable `start_with_keys`; or
- add more starting 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 StartWithAllMaps(Toggle):
"""
Whether to start with automaps for all included levels. If off they will be
added to the pool instead.
"""
display_name = "Start with all automaps"
default = True
class UZDoomStartInventory(StartInventory):
"""
Start with the specified amount of these items. You can list any valid
inventory item that the game engine understands, even if the randomizer
doesn't know about it, e.g. "BFG9000: 1" will work even if none of the
randomized maps contain 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)
###############################################################################
# Win conditions
###############################################################################
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()])
###############################################################################
# Combat logic
###############################################################################
def nrof_global_weapons(wad):
return len({
item.name() for item in wad.items()
if item.has_category('weapon') and not item.map})
def nrof_local_weapons(wad):
return len({
item.name() for item in wad.items()
if item.has_category('weapon') and item.map})
class PerMapWeaponUnlocks(Toggle):
__doc__ = f"""
Make weapon unlocks per-map instead of global. For example, a shotgun that
works on MAP01 and a shotgun that works on MAP02 will now be distinct items
in the pool.
The `max_weapon_copies` setting is ignored when this is enabled. There will
always be one copy of each weapon per map.
In this wad, with default settings, this will add {nrof_local_weapons(wad) - nrof_global_weapons(wad)} progression items to
the pool. If turning it on, you may need to add additional locations by
enabling small or medium items, unless you are playing in a multiworld with
other games that can absorb extra progression items.
"""
default = False
if wad.has_combat_logic_hints():
class CombatLogicMode(Choice):
"""
How the combat logic system works.
- off: no combat logic. Logic may still require certain weapons for
certain checks (e.g. rocket launcher for icon of sin).
- manual: the logic author for this wad has included combat logic data.
This setting will enable the use of it.
- auto_per_level: in addition to the manually authored combat logic,
this will consider weapons logically required for a level if they are
available anywhere in that level.
- auto_per_episode: as above, but considers weapons required if they are
available in a level or any of the preceding levels.
"""
display_name = "Combat logic mode"
default = 1
option_off = 0
option_manual = 1
option_auto_per_level = 2
option_auto_per_episode = 3
def is_disabled(self):
return self.value == 0
def is_enabled(self):
return not self.is_disabled()
def is_auto(self):
return self.value >= 2
else:
class CombatLogicMode(Choice):
"""
How the combat logic system works.
- off: no combat logic. Logic may still require certain weapons for
certain checks (e.g. rocket launcher for icon of sin).
- auto_per_level: this will consider weapons logically required for a
level if they are available anywhere in that level.
- auto_per_episode: as above, but considers weapons required if they are
available in a level or any of the preceding levels.
"""
display_name = "Combat logic mode"
default = 2
option_off = 0
option_auto_per_level = 2
option_auto_per_episode = 3
def is_disabled(self):
return self.value == 0
def is_enabled(self):
return not self.is_disabled()
def is_auto(self):
return self.value >= 2
class CombatLogicSecrets(Toggle):
"""
Whether to consider secrets in auto combat logic. If this is off, weapons
will not be considered logically required unless there is a non-secret way
to get them.
"""
display_name = "Auto combat logic uses secrets"
default = False
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
###############################################################################
# Gameplay settings
###############################################################################
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
###############################################################################
# Rando settings
###############################################################################
def items(*categories):
return {
name for category in categories
for name in included_logic.item_categories_to_names.get(category, [])
}
def locations(*categories):
return {
name for category in categories
for name in included_logic.location_categories_to_names.get(category, [])
}
def nrof(*categories):
return len(locations(*categories))
def names(*categories):
return ', '.join(sorted(items(*categories)))
def vis(*categories):
return Visibility.none if not nrof(*categories) else Visibility.all
def size_docs(size):
return f"""
What percentage of {size} items will be randomized.
This wad contains {nrof(size)} {size} items ({nrof(size+'-secret')} in secrets):
{names(size)}.
"""
def sized_names(size, category):
# We do this rather than using nrof/names because the categories_to_names
# maps are very particular about what order you put the category names in
# when constructing composite keys.
fqins = items(size) & items(category)
nlocs = len(locations(size) & locations(category))
return f'{size} ({nlocs}): {', '.join(sorted(fqins))}.\n' if len(fqins) else ''
def kind_docs(kind):
return f"""
Whether to include {kind} items in the pool. If off, items of this kind will
not be randomized. If on, they will be, subject to the size settings above.
This wad contains {nrof(kind)} {kind} items ({nrof(kind+'-secret')} in secrets):
{sized_names('big', kind)}{sized_names('medium', kind)}{sized_names('small', kind)}
"""
# Top-level toggle for whether secrets should be excluded.
class IncludeSecrets(Choice):
__doc__ = f"""
Whether secret locations are eligible for randomization.
If off, neither secrets themselves nor the items in them will be randomized.
If set to `items_only`, items located in secrets will be randomized, but the
secrets themselves will not be checks.
If set to `items_and_secrets`, items located in secrets will be randomized,
and finding the secret itself will also count as a check.
This wad contains {nrof('secret-sector', 'secret-marker')} secrets and {nrof('secret') - nrof('secret-sector', 'secret-marker')} secret items.
"""
display_name = "Secrets"
default = 0
option_excluded = 0
option_items_only = 1
option_items_and_secrets = 2
class IncludeItemAmount(NamedRange):
range_start = 0
range_end = 100
special_range_names = {
"all": 100,
"none": 0,
"shuffle": -1,
"vanilla": -2,
}
class IncludeBigItems(IncludeItemAmount):
__doc__ = f"""{size_docs('big')}
This option, and the ones below, supports some special values:
'shuffle' will randomize these items among themselves within your game,
without adding them to the pool. Unlike using plando to force these items
local, this will also not generate client messages when collecting them.
(Tuning data, however, will still be recorded.)
'vanilla' is similar to 'shuffle', except that each item will be placed at
its original vanilla location. It is primarily useful to logic developers.
"""
display_name = "Big items"
default = 100
visibility = vis('big')
class IncludeMediumItems(IncludeItemAmount):
__doc__ = size_docs('medium')
display_name = "Medium items"
default = -1
visibility = vis('medium')
class IncludeSmallItems(IncludeItemAmount):
__doc__ = size_docs('small')
display_name = "Small items"
default = -1
visibility = vis('small')
class IncludeUnknownSizeItems(IncludeItemAmount):
__doc__ = f"""
The logic for this wad contains items of unknown size. Report this to the
logic developer as a bug.
This wad contains {nrof('unknown_size')} unknown_size items:
{names('unknown_size')}.
"""
display_name = "Unknown-size items"
default = 100
visibility = vis('unknown_size')
class IncludeHealthItems(Toggle):
__doc__ = kind_docs('health')
display_name = "Health items"
default = True
visibility = vis('health')
class IncludeArmorItems(Toggle):
__doc__ = kind_docs('armor')
display_name = "Armour items"
default = True
visibility = vis('armor')
class IncludeAmmoItems(Toggle):
__doc__ = kind_docs('ammo')
display_name = "Ammo items"
default = True
visibility = vis('ammo')
class IncludeAttackItems(Toggle):
__doc__ = kind_docs('attack')
display_name = "Attack items"
default = True
visibility = vis('attack')
class IncludeDefenceItems(Toggle):
__doc__ = kind_docs('defence')
display_name = "Defence items"
default = True
visibility = vis('defence')
class IncludeToolItems(Toggle):
__doc__ = kind_docs('tool')
display_name = "Tool items"
default = True
visibility = vis('tool')
class IncludeUnknownKindItems(Toggle):
__doc__ = f"""
The logic for this wad contains items of unknown kind. Report this to the
logic developer as a bug.
This wad contains {nrof('unknown_kind')} unknown_kind items:
{names('unknown_kind')}.
"""
display_name = "Unknown-kind items"
default = True
visibility = vis('unknown_kind')
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 IncludedItemCategories(OptionList):
"""
This is a setting that offers low-level control over what locations get
randomized, for situations where the above settings do not suffice.
Each entry is a category (like 'health' or 'secret-big-armor'), followed by
':' and a percentage, e.g. 'health:50' to randomize 50% of all health items.
See doc/categories.md for a detailed explanation of officially supported
categories.
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'.
Instead of a percentage, you can also use 'all', 'none', 'shuffle', or
'vanilla', with the same meanings as above; or 'start' to place the matching
items directly into the player's starting inventory, although
start_inventory_from_pool is preferred for that.
Entries are checked left to right, with earlier ones taking precedence over
later ones; locations that match multiple entries will only affected by the
first one. So, for example, this setting:
['health:none', 'small:all', 'ammo:50']
Would randomize no health of any size, all big items, and 50% of whatever
ammo is left.
On a technical level, this setting is checked before anything else,
including UZArchipelago's built in baseline settings and the toggles above,
so it offers significant control over how the item and location pools are
populated, but also makes it easy to write settings that cannot generate.
"""
display_name = "Included item/location categories"
default = []
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:
continue
# 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))
# 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 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
@dataclass
class UZDoomOptions(PerGameCommonOptions):
# Start conditions
spawn_filter: SpawnFilter
starting_levels: StartingLevels
start_with_keys: StartWithKeys
included_levels: IncludedLevels
start_with_all_maps: StartWithAllMaps
start_inventory: UZDoomStartInventory # replaces stock start_inventory
# Win conditions
win_map_count: WinMapCount
win_map_names: WinMapNames
# Combat logic
per_map_weapons: PerMapWeaponUnlocks
combat_logic_mode: CombatLogicMode
combat_logic_secrets: CombatLogicSecrets
# Gameplay
allow_respawn: AllowRespawn
full_persistence: FullPersistence
# Location/item pool control
secrets: IncludeSecrets
big_items: IncludeBigItems
medium_items: IncludeMediumItems
small_items: IncludeSmallItems
unknown_size_items: IncludeUnknownSizeItems
health_items: IncludeHealthItems
armor_items: IncludeArmorItems
ammo_items: IncludeAmmoItems
attack_items: IncludeAttackItems
defence_items: IncludeDefenceItems
tool_items: IncludeToolItems
unknown_kind_items: IncludeUnknownKindItems
max_weapon_copies: MaxWeaponCopies
included_item_categories: IncludedItemCategories
pretuning_mode: PreTuningMode
# Stock settings
start_inventory_from_pool: StartInventoryPool
class UZDoomWeb(WebWorld):
game = f"UZDoom ({wad.name})"
option_groups = [
OptionGroup("Starting Conditions", [
SpawnFilter, StartingLevels, StartWithKeys, IncludedLevels, StartWithAllMaps, UZDoomStartInventory,
]),
OptionGroup("Win Conditions", [
WinMapCount, WinMapNames,
]),
OptionGroup("Combat Logic", [
PerMapWeaponUnlocks, CombatLogicMode, CombatLogicSecrets
]),
OptionGroup("Gameplay Settings", [
AllowRespawn, FullPersistence,
]),
OptionGroup("Randomization Settings", [
IncludeSecrets, IncludeBigItems, IncludeMediumItems, IncludeSmallItems,
IncludeHealthItems, IncludeArmorItems, IncludeAmmoItems,
IncludeAttackItems, IncludeDefenceItems, IncludeToolItems,
MaxWeaponCopies, IncludedItemCategories, PreTuningMode,
]),
]
# 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