id Tech Games: Customizable ammo capacity (#3565)

* Doom, Doom 2, Heretic: customizable ammo capacity

* Do not progression balance capacity up items

* Prog fill still doesn't agree, just go with our original idea

* Clean up the new options a bit

- Gave all options a consistent and easily readable naming scheme
  (`max_ammo_<type>` and `added_ammo_<type>`)
- Don't show the new options in the spoiler log,
  as they do not affect logic
- Fix the Doom games' Split Backpack option accidentally referring to
  Heretic's Bag of Holding

The logging change across all three games is incidental, as at some
point I did run into that condition by happenstance and it turns out
that it throws an exception due to bad formatting if it's reached

* Do the visibility change for Heretic as well

* Update required client version

* Remove spoiler log restriction on options

* Remove Visibility import now made redundant
This commit is contained in:
Kaito Sinclaire
2025-03-08 07:37:54 -08:00
committed by GitHub
parent 00a6ac3a52
commit b5269e9aa4
10 changed files with 469 additions and 19 deletions

View File

@@ -650,8 +650,8 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2006,
'episode': -1,
'map': -1},
350106: {'classification': ItemClassification.progression,
'count': 1,
350106: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Backpack',
'doom_type': 8,
'episode': -1,
@@ -1160,6 +1160,30 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2026,
'episode': 4,
'map': 9},
350191: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Bullet capacity',
'doom_type': 65001,
'episode': -1,
'map': -1},
350192: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Shell capacity',
'doom_type': 65002,
'episode': -1,
'map': -1},
350193: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Energy cell capacity',
'doom_type': 65003,
'episode': -1,
'map': -1},
350194: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Rocket capacity',
'doom_type': 65004,
'episode': -1,
'map': -1},
}

View File

@@ -1,4 +1,4 @@
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from dataclasses import dataclass
@@ -144,6 +144,84 @@ class Episode4(Toggle):
display_name = "Episode 4"
class SplitBackpack(Toggle):
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
display_name = "Split Backpack"
class BackpackCount(Range):
"""How many Backpacks will be available.
If Split Backpack is set, this will be the number of each capacity upgrade available."""
display_name = "Backpack Count"
range_start = 0
range_end = 10
default = 1
class MaxAmmoBullets(Range):
"""Set the starting ammo capacity for bullets."""
display_name = "Max Ammo - Bullets"
range_start = 200
range_end = 999
default = 200
class MaxAmmoShells(Range):
"""Set the starting ammo capacity for shotgun shells."""
display_name = "Max Ammo - Shells"
range_start = 50
range_end = 999
default = 50
class MaxAmmoRockets(Range):
"""Set the starting ammo capacity for rockets."""
display_name = "Max Ammo - Rockets"
range_start = 50
range_end = 999
default = 50
class MaxAmmoEnergyCells(Range):
"""Set the starting ammo capacity for energy cells."""
display_name = "Max Ammo - Energy Cells"
range_start = 300
range_end = 999
default = 300
class AddedAmmoBullets(Range):
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Bullets"
range_start = 20
range_end = 999
default = 200
class AddedAmmoShells(Range):
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Shells"
range_start = 5
range_end = 999
default = 50
class AddedAmmoRockets(Range):
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Rockets"
range_start = 5
range_end = 999
default = 50
class AddedAmmoEnergyCells(Range):
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Energy Cells"
range_start = 30
range_end = 999
default = 300
@dataclass
class DOOM1993Options(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -163,3 +241,14 @@ class DOOM1993Options(PerGameCommonOptions):
episode3: Episode3
episode4: Episode4
split_backpack: SplitBackpack
backpack_count: BackpackCount
max_ammo_bullets: MaxAmmoBullets
max_ammo_shells: MaxAmmoShells
max_ammo_rockets: MaxAmmoRockets
max_ammo_energy_cells: MaxAmmoEnergyCells
added_ammo_bullets: AddedAmmoBullets
added_ammo_shells: AddedAmmoShells
added_ammo_rockets: AddedAmmoRockets
added_ammo_energy_cells: AddedAmmoEnergyCells

View File

@@ -42,7 +42,7 @@ class DOOM1993World(World):
options: DOOM1993Options
game = "DOOM 1993"
web = DOOM1993Web()
required_client_version = (0, 3, 9)
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
item_name_groups = Items.item_name_groups
@@ -204,6 +204,15 @@ class DOOM1993World(World):
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Backpack(s) based on options
if self.options.split_backpack.value:
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
else:
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
# Place end level items in locked locations
for map_name in Maps.map_names:
loc_name = map_name + " - Exit"
@@ -265,7 +274,7 @@ class DOOM1993World(World):
# Was balanced for 3 episodes (We added 4th episode, but keep same ratio)
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
if count == 0:
logger.warning("Warning, no ", item_name, " will be placed.")
logger.warning(f"Warning, no {item_name} will be placed.")
return
for i in range(count):
@@ -281,4 +290,14 @@ class DOOM1993World(World):
# an older version, the player would end up stuck.
slot_data["two_ways_keydoors"] = True
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
slot_data["ammo2start"] = self.options.max_ammo_shells.value
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
slot_data["ammo2add"] = self.options.added_ammo_shells.value
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
return slot_data

View File

@@ -56,8 +56,8 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 82,
'episode': -1,
'map': -1},
360007: {'classification': ItemClassification.progression,
'count': 1,
360007: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Backpack',
'doom_type': 8,
'episode': -1,
@@ -1058,6 +1058,30 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2026,
'episode': 4,
'map': 2},
360600: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Bullet capacity',
'doom_type': 65001,
'episode': -1,
'map': -1},
360601: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Shell capacity',
'doom_type': 65002,
'episode': -1,
'map': -1},
360602: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Energy cell capacity',
'doom_type': 65003,
'episode': -1,
'map': -1},
360603: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Rocket capacity',
'doom_type': 65004,
'episode': -1,
'map': -1},
}

View File

@@ -1,6 +1,6 @@
import typing
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from dataclasses import dataclass
@@ -136,6 +136,84 @@ class SecretLevels(Toggle):
display_name = "Secret Levels"
class SplitBackpack(Toggle):
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
display_name = "Split Backpack"
class BackpackCount(Range):
"""How many Backpacks will be available.
If Split Backpack is set, this will be the number of each capacity upgrade available."""
display_name = "Backpack Count"
range_start = 0
range_end = 10
default = 1
class MaxAmmoBullets(Range):
"""Set the starting ammo capacity for bullets."""
display_name = "Max Ammo - Bullets"
range_start = 200
range_end = 999
default = 200
class MaxAmmoShells(Range):
"""Set the starting ammo capacity for shotgun shells."""
display_name = "Max Ammo - Shells"
range_start = 50
range_end = 999
default = 50
class MaxAmmoRockets(Range):
"""Set the starting ammo capacity for rockets."""
display_name = "Max Ammo - Rockets"
range_start = 50
range_end = 999
default = 50
class MaxAmmoEnergyCells(Range):
"""Set the starting ammo capacity for energy cells."""
display_name = "Max Ammo - Energy Cells"
range_start = 300
range_end = 999
default = 300
class AddedAmmoBullets(Range):
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Bullets"
range_start = 20
range_end = 999
default = 200
class AddedAmmoShells(Range):
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Shells"
range_start = 5
range_end = 999
default = 50
class AddedAmmoRockets(Range):
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Rockets"
range_start = 5
range_end = 999
default = 50
class AddedAmmoEnergyCells(Range):
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Energy Cells"
range_start = 30
range_end = 999
default = 300
@dataclass
class DOOM2Options(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -153,3 +231,14 @@ class DOOM2Options(PerGameCommonOptions):
episode2: Episode2
episode3: Episode3
episode4: SecretLevels
split_backpack: SplitBackpack
backpack_count: BackpackCount
max_ammo_bullets: MaxAmmoBullets
max_ammo_shells: MaxAmmoShells
max_ammo_rockets: MaxAmmoRockets
max_ammo_energy_cells: MaxAmmoEnergyCells
added_ammo_bullets: AddedAmmoBullets
added_ammo_shells: AddedAmmoShells
added_ammo_rockets: AddedAmmoRockets
added_ammo_energy_cells: AddedAmmoEnergyCells

View File

@@ -43,7 +43,7 @@ class DOOM2World(World):
options: DOOM2Options
game = "DOOM II"
web = DOOM2Web()
required_client_version = (0, 3, 9)
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
item_name_groups = Items.item_name_groups
@@ -196,6 +196,15 @@ class DOOM2World(World):
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Backpack(s) based on options
if self.options.split_backpack.value:
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
else:
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
# Place end level items in locked locations
for map_name in Maps.map_names:
loc_name = map_name + " - Exit"
@@ -258,11 +267,23 @@ class DOOM2World(World):
# Was balanced based on DOOM 1993's first 3 episodes
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
if count == 0:
logger.warning("Warning, no ", item_name, " will be placed.")
logger.warning(f"Warning, no {item_name} will be placed.")
return
for i in range(count):
itempool.append(self.create_item(item_name))
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
slot_data = self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
slot_data["ammo2start"] = self.options.max_ammo_shells.value
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
slot_data["ammo2add"] = self.options.added_ammo_shells.value
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
return slot_data

View File

@@ -50,8 +50,8 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2004,
'episode': -1,
'map': -1},
370006: {'classification': ItemClassification.progression,
'count': 1,
370006: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Bag of Holding',
'doom_type': 8,
'episode': -1,
@@ -1592,6 +1592,42 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 35,
'episode': 5,
'map': 9},
370600: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Crystal Capacity',
'doom_type': 65001,
'episode': -1,
'map': -1},
370601: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Ethereal Arrow Capacity',
'doom_type': 65002,
'episode': -1,
'map': -1},
370602: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Claw Orb Capacity',
'doom_type': 65003,
'episode': -1,
'map': -1},
370603: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Rune Capacity',
'doom_type': 65004,
'episode': -1,
'map': -1},
370604: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Flame Orb Capacity',
'doom_type': 65005,
'episode': -1,
'map': -1},
370605: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Mace Sphere Capacity',
'doom_type': 65006,
'episode': -1,
'map': -1},
}

View File

@@ -1,4 +1,4 @@
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from dataclasses import dataclass
@@ -144,6 +144,116 @@ class Episode5(Toggle):
display_name = "Episode 5"
class SplitBagOfHolding(Toggle):
"""Split the Bag of Holding into six individual items, each one increasing ammo capacity for one type of weapon only."""
display_name = "Split Bag of Holding"
class BagOfHoldingCount(Range):
"""How many Bags of Holding will be available.
If Split Bag of Holding is set, this will be the number of each capacity upgrade available."""
display_name = "Bag of Holding Count"
range_start = 0
range_end = 10
default = 1
class MaxAmmoCrystals(Range):
"""Set the starting ammo capacity for crystals (Elven Wand ammo)."""
display_name = "Max Ammo - Crystals"
range_start = 100
range_end = 999
default = 100
class MaxAmmoArrows(Range):
"""Set the starting ammo capacity for arrows (Ethereal Crossbow ammo)."""
display_name = "Max Ammo - Arrows"
range_start = 50
range_end = 999
default = 50
class MaxAmmoClawOrbs(Range):
"""Set the starting ammo capacity for claw orbs (Dragon Claw ammo)."""
display_name = "Max Ammo - Claw Orbs"
range_start = 200
range_end = 999
default = 200
class MaxAmmoRunes(Range):
"""Set the starting ammo capacity for runes (Hellstaff ammo)."""
display_name = "Max Ammo - Runes"
range_start = 200
range_end = 999
default = 200
class MaxAmmoFlameOrbs(Range):
"""Set the starting ammo capacity for flame orbs (Phoenix Rod ammo)."""
display_name = "Max Ammo - Flame Orbs"
range_start = 20
range_end = 999
default = 20
class MaxAmmoSpheres(Range):
"""Set the starting ammo capacity for spheres (Firemace ammo)."""
display_name = "Max Ammo - Spheres"
range_start = 150
range_end = 999
default = 150
class AddedAmmoCrystals(Range):
"""Set the amount of crystal capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Crystals"
range_start = 10
range_end = 999
default = 100
class AddedAmmoArrows(Range):
"""Set the amount of arrow capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Arrows"
range_start = 5
range_end = 999
default = 50
class AddedAmmoClawOrbs(Range):
"""Set the amount of claw orb capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Claw Orbs"
range_start = 20
range_end = 999
default = 200
class AddedAmmoRunes(Range):
"""Set the amount of rune capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Runes"
range_start = 20
range_end = 999
default = 200
class AddedAmmoFlameOrbs(Range):
"""Set the amount of flame orb capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Flame Orbs"
range_start = 2
range_end = 999
default = 20
class AddedAmmoSpheres(Range):
"""Set the amount of sphere capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Spheres"
range_start = 15
range_end = 999
default = 150
@dataclass
class HereticOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -163,3 +273,18 @@ class HereticOptions(PerGameCommonOptions):
episode3: Episode3
episode4: Episode4
episode5: Episode5
split_bag_of_holding: SplitBagOfHolding
bag_of_holding_count: BagOfHoldingCount
max_ammo_crystals: MaxAmmoCrystals
max_ammo_arrows: MaxAmmoArrows
max_ammo_claw_orbs: MaxAmmoClawOrbs
max_ammo_runes: MaxAmmoRunes
max_ammo_flame_orbs: MaxAmmoFlameOrbs
max_ammo_spheres: MaxAmmoSpheres
added_ammo_crystals: AddedAmmoCrystals
added_ammo_arrows: AddedAmmoArrows
added_ammo_claw_orbs: AddedAmmoClawOrbs
added_ammo_runes: AddedAmmoRunes
added_ammo_flame_orbs: AddedAmmoFlameOrbs
added_ammo_spheres: AddedAmmoSpheres

View File

@@ -695,13 +695,11 @@ def set_episode5_rules(player, multiworld, pro):
state.has("Phoenix Rod", player, 1) and
state.has("Firemace", player, 1) and
state.has("Hellstaff", player, 1) and
state.has("Gauntlets of the Necromancer", player, 1) and
state.has("Bag of Holding", player, 1))
state.has("Gauntlets of the Necromancer", player, 1))
# Skein of D'Sparil (E5M9)
set_rule(multiworld.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state:
state.has("Skein of D'Sparil (E5M9)", player, 1) and
state.has("Bag of Holding", player, 1) and
state.has("Hellstaff", player, 1) and
state.has("Phoenix Rod", player, 1) and
state.has("Dragon Claw", player, 1) and

View File

@@ -41,7 +41,7 @@ class HereticWorld(World):
options: HereticOptions
game = "Heretic"
web = HereticWeb()
required_client_version = (0, 3, 9)
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
item_name_groups = Items.item_name_groups
@@ -206,6 +206,17 @@ class HereticWorld(World):
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Bag(s) of Holding based on options
if self.options.split_bag_of_holding.value:
itempool += [self.create_item("Crystal Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Ethereal Arrow Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Claw Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Rune Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Flame Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Mace Sphere Capacity") for _ in range(self.options.bag_of_holding_count.value)]
else:
itempool += [self.create_item("Bag of Holding") for _ in range(self.options.bag_of_holding_count.value)]
# Place end level items in locked locations
for map_name in Maps.map_names:
loc_name = map_name + " - Exit"
@@ -274,7 +285,7 @@ class HereticWorld(World):
episode_count = self.get_episode_count()
count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count))
if count == 0:
logger.warning("Warning, no " + item_name + " will be placed.")
logger.warning(f"Warning, no {item_name} will be placed.")
return
for i in range(count):
@@ -290,4 +301,18 @@ class HereticWorld(World):
slot_data["episode4"] = self.included_episodes[3]
slot_data["episode5"] = self.included_episodes[4]
# Send slot data for ammo capacity values; this must be generic because Doom uses it too
slot_data["ammo1start"] = self.options.max_ammo_crystals.value
slot_data["ammo2start"] = self.options.max_ammo_arrows.value
slot_data["ammo3start"] = self.options.max_ammo_claw_orbs.value
slot_data["ammo4start"] = self.options.max_ammo_runes.value
slot_data["ammo5start"] = self.options.max_ammo_flame_orbs.value
slot_data["ammo6start"] = self.options.max_ammo_spheres.value
slot_data["ammo1add"] = self.options.added_ammo_crystals.value
slot_data["ammo2add"] = self.options.added_ammo_arrows.value
slot_data["ammo3add"] = self.options.added_ammo_claw_orbs.value
slot_data["ammo4add"] = self.options.added_ammo_runes.value
slot_data["ammo5add"] = self.options.added_ammo_flame_orbs.value
slot_data["ammo6add"] = self.options.added_ammo_spheres.value
return slot_data