Compare commits

...

22 Commits

Author SHA1 Message Date
CaitSith2
2524ddc075 Dark Souls 3: Use options_dataclass 2023-12-24 17:30:53 -08:00
CaitSith2
d545b78803 Clique: Use options_dataclass 2023-12-24 17:30:17 -08:00
CaitSith2
88b1c94eb2 ChecksFinder: use options_dataclass 2023-12-24 17:29:30 -08:00
CaitSith2
7742d5d804 BumperStickers: Use options_dataclass 2023-12-24 17:29:04 -08:00
CaitSith2
d3e148dcc6 Blasphemous: Use options_dataclass 2023-12-24 17:28:34 -08:00
CaitSith2
b5fccde913 Didn't mean to include this debugging line. 2023-12-24 16:50:40 -08:00
CaitSith2
55e9b0687a Factorio: Options assigned as data_class 2023-12-24 16:49:05 -08:00
CaitSith2
79e1bf351e Adventure: options assigned as data_class 2023-12-24 16:47:54 -08:00
CaitSith2
fcfea9d9aa Adventure: use options.name instead of multiworld.name[players] 2023-12-24 15:22:07 -08:00
CaitSith2
cfc5508f06 ds3: use options.name instead of multiworld.name[player] 2023-12-24 14:10:33 -08:00
CaitSith2
62cb5f1fc2 fix flake8(push) hopefully 2023-12-24 13:43:19 -08:00
CaitSith2
7e70b16656 Clique: use options.name instead of multiworld.name 2023-12-24 13:34:25 -08:00
CaitSith2
7b486b3380 BumpStick: use options.name instead of multiworld.name 2023-12-24 13:33:32 -08:00
CaitSith2
09cac0a685 Core: use options.option_name instead of multiworld.option_name 2023-12-24 12:24:31 -08:00
CaitSith2
12c583533d Blasphemous: use options.option_name instead of multiworld.option_name 2023-12-24 11:43:28 -08:00
CaitSith2
c5af28a649 Factorio: use options.option_name instead of multiworld.option_name 2023-12-24 11:41:23 -08:00
CaitSith2
bb0a0f2aca Factorio: Fix unbeatable seeds where a science pack needs chemical plant (#2613) 2023-12-22 20:02:49 -08:00
Fabian Dill
0d929b81e8 Factorio: fix files from mod base directory not being grabbed correctly in non-apworld (#2603) 2023-12-21 04:26:41 +01:00
Fabian Dill
8842f5d5c7 Core: make update_reachable_regions local variables more wordy (#2522) 2023-12-21 04:11:11 +01:00
Star Rauchenberger
817197c14d Lingo: Tests no longer disable forced good item (#2602)
The static class with the "disable forced good item" field is gone. Now, certain tests that want to check for specific access progression can run a method that removes the forced good item and adds it back to the pool. Tests that don't care about this will collect the forced good item like normal. This should prevent the intermittent fill failures on complex doors unit tests, since the forced good item should provide enough locations to fill in.
2023-12-18 09:46:24 -06:00
Alchav
c8adadb08b Pokémon R/B: Fix Flash learnable logic (#2615) 2023-12-18 09:39:04 -06:00
Zach Parks
a549af8304 Hollow Knight: Add additional DeathLink option and add ExtraPlatforms option. (#2545) 2023-12-17 10:11:40 -06:00
34 changed files with 495 additions and 436 deletions

View File

@@ -651,34 +651,34 @@ class CollectionState():
def update_reachable_regions(self, player: int):
self.stale[player] = False
rrp = self.reachable_regions[player]
bc = self.blocked_connections[player]
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player])
start = self.multiworld.get_region('Menu', player)
start = self.multiworld.get_region("Menu", player)
# init on first call - this can't be done on construction since the regions don't exist yet
if start not in rrp:
rrp.add(start)
bc.update(start.exits)
if start not in reachable_regions:
reachable_regions.add(start)
blocked_connections.update(start.exits)
queue.extend(start.exits)
# run BFS on all connections, and keep track of those blocked by missing items
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region in rrp:
bc.remove(connection)
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
rrp.add(new_region)
bc.remove(connection)
bc.update(new_region.exits)
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
if new_entrance in bc and new_entrance not in queue:
if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance)
def copy(self) -> CollectionState:

View File

@@ -167,10 +167,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value for player in world.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
player: getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value.copy()
for player in world.player_ids}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player]
for count in items.values():

View File

@@ -2,7 +2,9 @@ from __future__ import annotations
from typing import Dict
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
from dataclasses import dataclass
class FreeincarnateMax(Range):
@@ -224,21 +226,20 @@ class StartCastle(Choice):
default = option_yellow
adventure_option_definitions: Dict[str, type(Option)] = {
"dragon_slay_check": DragonSlayCheck,
"death_link": DeathLink,
"bat_logic": BatLogic,
"freeincarnate_max": FreeincarnateMax,
"dragon_rando_type": DragonRandoType,
"connector_multi_slot": ConnectorMultiSlot,
"yorgle_speed": YorgleStartingSpeed,
"yorgle_min_speed": YorgleMinimumSpeed,
"grundle_speed": GrundleStartingSpeed,
"grundle_min_speed": GrundleMinimumSpeed,
"rhindle_speed": RhindleStartingSpeed,
"rhindle_min_speed": RhindleMinimumSpeed,
"difficulty_switch_a": DifficultySwitchA,
"difficulty_switch_b": DifficultySwitchB,
"start_castle": StartCastle,
}
@dataclass
class AdventureOptions(PerGameCommonOptions):
dragon_slay_check: DragonSlayCheck
death_link: DeathLink
bat_logic: BatLogic
freeincarnate_max: FreeincarnateMax
dragon_rando_type: DragonRandoType
connector_multi_slot: ConnectorMultiSlot
yorgle_speed: YorgleStartingSpeed
yorgle_min_speed: YorgleMinimumSpeed
grundle_speed: GrundleStartingSpeed
grundle_min_speed: GrundleMinimumSpeed
rhindle_speed: RhindleStartingSpeed
rhindle_min_speed: RhindleMinimumSpeed
difficulty_switch_a: DifficultySwitchA
difficulty_switch_b: DifficultySwitchB
start_castle: StartCastle

View File

@@ -1,5 +1,6 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
from Options import PerGameCommonOptions
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
@@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
connect(world, player, target, source, rule, True)
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
for name, locdata in location_table.items():
locdata.get_position(multiworld.random)
@@ -76,7 +77,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
multiworld.regions.append(credits_room_far_side)
dragon_slay_check = multiworld.dragon_slay_check[player].value
dragon_slay_check = options.dragon_slay_check.value
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items():

View File

@@ -6,7 +6,8 @@ from BaseClasses import LocationProgressType
def set_rules(self) -> None:
world = self.multiworld
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
options = self.options
use_bat_logic = options.bat_logic.value == BatLogic.option_use_logic
set_rule(world.get_entrance("YellowCastlePort", self.player),
lambda state: state.has("Yellow Key", self.player))
@@ -28,7 +29,7 @@ def set_rules(self) -> None:
lambda state: state.has("Bridge", self.player) or
state.has("Magnet", self.player))
dragon_slay_check = world.dragon_slay_check[self.player].value
dragon_slay_check = options.dragon_slay_check.value
if dragon_slay_check:
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
set_rule(world.get_location("Slay Yorgle", self.player),

View File

@@ -15,7 +15,7 @@ from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
from .Options import AdventureOptions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
@@ -109,7 +109,8 @@ class AdventureWorld(World):
game: ClassVar[str] = "Adventure"
web: ClassVar[WebWorld] = AdventureWeb()
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
options = AdventureOptions
options_dataclass = AdventureOptions
settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
@@ -150,18 +151,18 @@ class AdventureWorld(World):
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name)))
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
self.start_castle = self.multiworld.start_castle[self.player].value
self.dragon_rando_type = self.options.dragon_rando_type.value
self.dragon_slay_check = self.options.dragon_slay_check.value
self.connector_multi_slot = self.options.connector_multi_slot.value
self.yorgle_speed = self.options.yorgle_speed.value
self.yorgle_min_speed = self.options.yorgle_min_speed.value
self.grundle_speed = self.options.grundle_speed.value
self.grundle_min_speed = self.options.grundle_min_speed.value
self.rhindle_speed = self.options.rhindle_speed.value
self.rhindle_min_speed = self.options.rhindle_min_speed.value
self.difficulty_switch_a = self.options.difficulty_switch_a.value
self.difficulty_switch_b = self.options.difficulty_switch_b.value
self.start_castle = self.options.start_castle.value
self.created_items = 0
if self.dragon_slay_check == 0:
@@ -228,7 +229,7 @@ class AdventureWorld(World):
extra_filler_count = num_locations - self.created_items
# traps would probably go here, if enabled
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
freeincarnate_max = self.options.freeincarnate_max.value
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
self.created_items += actual_freeincarnates
@@ -248,7 +249,7 @@ class AdventureWorld(World):
self.created_items += 1
def create_regions(self) -> None:
create_regions(self.multiworld, self.player, self.dragon_rooms)
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules
@@ -355,7 +356,7 @@ class AdventureWorld(World):
auto_collect_locations: [AdventureAutoCollectLocation] = []
local_item_to_location: {int, int} = {}
bat_no_touch_locs: [LocationData] = []
bat_logic: int = self.multiworld.bat_logic[self.player].value
bat_logic: int = self.options.bat_logic.value
try:
rom_deltas: { int, int } = {}
self.place_dragons(rom_deltas)
@@ -411,7 +412,7 @@ class AdventureWorld(World):
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
rom_deltas[item_position_data_start] = 0xff
if self.multiworld.connector_multi_slot[self.player].value:
if self.options.connector_multi_slot.value:
rom_deltas[connector_port_offset] = (self.player & 0xff)
else:
rom_deltas[connector_port_offset] = 0

View File

@@ -1,4 +1,5 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PerGameCommonOptions
from dataclasses import dataclass
import random
@@ -163,26 +164,26 @@ class BlasphemousDeathLink(DeathLink):
Note that Guilt Fragments will not appear when killed by Death Link."""
blasphemous_options = {
"prie_dieu_warp": PrieDieuWarp,
"skip_cutscenes": SkipCutscenes,
"corpse_hints": CorpseHints,
"difficulty": Difficulty,
"penitence": Penitence,
"starting_location": StartingLocation,
"ending": Ending,
"skip_long_quests": SkipLongQuests,
"thorn_shuffle" : ThornShuffle,
"dash_shuffle": DashShuffle,
"wall_climb_shuffle": WallClimbShuffle,
"reliquary_shuffle": ReliquaryShuffle,
"boots_of_pleading": CustomItem1,
"purified_hand": CustomItem2,
"start_wheel": StartWheel,
"skill_randomizer": SkillRando,
"enemy_randomizer": EnemyRando,
"enemy_groups": EnemyGroups,
"enemy_scaling": EnemyScaling,
"death_link": BlasphemousDeathLink,
"start_inventory": StartInventoryPool
}
@dataclass
class BlasphemousOptions(PerGameCommonOptions):
prie_dieu_warp: PrieDieuWarp
skip_cutscenes: SkipCutscenes
corpse_hints: CorpseHints
difficulty: Difficulty
penitence: Penitence
starting_location: StartingLocation
ending: Ending
skip_long_quests: SkipLongQuests
thorn_shuffle : ThornShuffle
dash_shuffle: DashShuffle
wall_climb_shuffle: WallClimbShuffle
reliquary_shuffle: ReliquaryShuffle
boots_of_pleading: CustomItem1
purified_hand: CustomItem2
start_wheel: StartWheel
skill_randomizer: SkillRando
enemy_randomizer: EnemyRando
enemy_groups: EnemyGroups
enemy_scaling: EnemyScaling
death_link: BlasphemousDeathLink
start_inventory: StartInventoryPool

View File

@@ -497,8 +497,9 @@ def chalice_rooms(state: CollectionState, player: int, number: int) -> bool:
def rules(blasphemousworld):
world = blasphemousworld.multiworld
player = blasphemousworld.player
logic = world.difficulty[player].value
enemy = world.enemy_randomizer[player].value
options = blasphemousworld.options
logic = options.difficulty.value
enemy = options.enemy_randomizer.value
# D01Z01S01 (The Holy Line)
@@ -2488,7 +2489,7 @@ def rules(blasphemousworld):
# D04Z02S01 (Mother of Mothers)
# Items
if world.purified_hand[player]:
if options.purified_hand:
set_rule(world.get_location("MoM: Western room ledge", player),
lambda state: (
state.has("D04Z02S01[N]", player)
@@ -4093,7 +4094,7 @@ def rules(blasphemousworld):
# D17Z01S04 (Brotherhood of the Silent Sorrow)
# Items
if world.boots_of_pleading[player]:
if options.boots_of_pleading:
set_rule(world.get_location("BotSS: 2nd meeting with Redento", player),
lambda state: redento(state, blasphemousworld, player, 2))
# Doors

View File

@@ -7,7 +7,7 @@ from .Locations import location_table
from .Rooms import room_table, door_table
from .Rules import rules
from worlds.generic.Rules import set_rule, add_rule
from .Options import blasphemous_options
from .Options import BlasphemousOptions
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
@@ -39,7 +39,8 @@ class BlasphemousWorld(World):
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
item_name_groups = group_table
option_definitions = blasphemous_options
options = BlasphemousOptions
options_dataclass = BlasphemousOptions
required_client_version = (0, 4, 2)
@@ -73,60 +74,61 @@ class BlasphemousWorld(World):
def generate_early(self):
options = self.options
world = self.multiworld
player = self.player
if not world.starting_location[player].randomized:
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
if not options.starting_location.randomized:
if options.starting_location.value == 6 and options.difficulty.value < 2:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
" cannot be chosen if Difficulty is lower than Hard.")
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
and world.dash_shuffle[player]:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
if (options.starting_location.value == 0 or options.starting_location.value == 6) \
and options.dash_shuffle:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
" cannot be chosen if Shuffle Dash is enabled.")
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
if options.starting_location.value == 3 and options.wall_climb_shuffle:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
" cannot be chosen if Shuffle Wall Climb is enabled.")
else:
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
invalid: bool = False
if world.difficulty[player].value < 2:
if options.difficulty.value < 2:
locations.remove(6)
if world.dash_shuffle[player]:
if options.dash_shuffle:
locations.remove(0)
if 6 in locations:
locations.remove(6)
if world.wall_climb_shuffle[player]:
if options.wall_climb_shuffle:
locations.remove(3)
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
if options.starting_location.value == 6 and options.difficulty.value < 2:
invalid = True
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
and world.dash_shuffle[player]:
if (options.starting_location.value == 0 or options.starting_location.value == 6) \
and options.dash_shuffle:
invalid = True
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
if options.starting_location.value == 3 and options.wall_climb_shuffle:
invalid = True
if invalid:
world.starting_location[player].value = world.random.choice(locations)
options.starting_location.value = world.random.choice(locations)
if not world.dash_shuffle[player]:
if not options.dash_shuffle:
world.push_precollected(self.create_item("Dash Ability"))
if not world.wall_climb_shuffle[player]:
if not options.wall_climb_shuffle:
world.push_precollected(self.create_item("Wall Climb Ability"))
if world.skip_long_quests[player]:
if options.skip_long_quests:
for loc in junk_locations:
world.exclude_locations[player].value.add(loc)
options.exclude_locations.value.add(loc)
start_rooms: Dict[int, str] = {
0: "D17Z01S01",
@@ -138,12 +140,12 @@ class BlasphemousWorld(World):
6: "D20Z02S09"
}
self.start_room = start_rooms[world.starting_location[player].value]
self.start_room = start_rooms[options.starting_location.value]
def create_items(self):
options = self.options
world = self.multiworld
player = self.player
removed: int = 0
to_remove: List[str] = [
@@ -157,46 +159,46 @@ class BlasphemousWorld(World):
skipped_items = []
junk: int = 0
for item, count in world.start_inventory[player].value.items():
for item, count in options.start_inventory.value.items():
for _ in range(count):
skipped_items.append(item)
junk += 1
skipped_items.extend(unrandomized_dict.values())
if world.thorn_shuffle[player] == 2:
if options.thorn_shuffle == 2:
for i in range(8):
skipped_items.append("Thorn Upgrade")
if world.dash_shuffle[player]:
if options.dash_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.dash_shuffle[player]:
elif not options.dash_shuffle:
skipped_items.append("Dash Ability")
if world.wall_climb_shuffle[player]:
if options.wall_climb_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.wall_climb_shuffle[player]:
elif not options.wall_climb_shuffle:
skipped_items.append("Wall Climb Ability")
if not world.reliquary_shuffle[player]:
if not options.reliquary_shuffle:
skipped_items.extend(reliquary_set)
elif world.reliquary_shuffle[player]:
elif options.reliquary_shuffle:
for i in range(3):
skipped_items.append(to_remove[removed])
removed += 1
if not world.boots_of_pleading[player]:
if not options.boots_of_pleading:
skipped_items.append("Boots of Pleading")
if not world.purified_hand[player]:
if not options.purified_hand:
skipped_items.append("Purified Hand of the Nun")
if world.start_wheel[player]:
if options.start_wheel:
skipped_items.append("The Young Mason's Wheel")
if not world.skill_randomizer[player]:
if not options.skill_randomizer:
skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items)
@@ -219,23 +221,24 @@ class BlasphemousWorld(World):
def pre_fill(self):
options = self.options
world = self.multiworld
player = self.player
self.place_items_from_dict(unrandomized_dict)
if world.thorn_shuffle[player] == 2:
if options.thorn_shuffle == 2:
self.place_items_from_set(thorn_set, "Thorn Upgrade")
if world.start_wheel[player]:
if options.start_wheel:
world.get_location("Beginning gift", player)\
.place_locked_item(self.create_item("The Young Mason's Wheel"))
if not world.skill_randomizer[player]:
if not options.skill_randomizer:
self.place_items_from_dict(skill_dict)
if world.thorn_shuffle[player] == 1:
world.local_items[player].value.add("Thorn Upgrade")
if options.thorn_shuffle == 1:
options.local_items.value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str):
@@ -251,6 +254,7 @@ class BlasphemousWorld(World):
def create_regions(self) -> None:
options = self.options
player = self.player
world = self.multiworld
@@ -282,9 +286,9 @@ class BlasphemousWorld(World):
})
for index, loc in enumerate(location_table):
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
if not options.boots_of_pleading and loc["name"] == "BotSS: 2nd meeting with Redento":
continue
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
if not options.purified_hand and loc["name"] == "MoM: Western room ledge":
continue
region: Region = world.get_region(loc["room"], player)
@@ -310,9 +314,9 @@ class BlasphemousWorld(World):
victory.place_locked_item(self.create_event("Victory"))
world.get_region("D07Z01S03", player).locations.append(victory)
if world.ending[self.player].value == 1:
if options.ending.value == 1:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif world.ending[self.player].value == 2:
elif options.ending.value == 2:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
state.has("Holy Wound of Abnegation", player))
@@ -332,11 +336,12 @@ class BlasphemousWorld(World):
locations = []
doors: Dict[str, str] = {}
options = self.options
world = self.multiworld
player = self.player
thorns: bool = True
if world.thorn_shuffle[player].value == 2:
if options.thorn_shuffle.value == 2:
thorns = False
for loc in world.get_filled_locations(player):
@@ -354,28 +359,28 @@ class BlasphemousWorld(World):
locations.append(data)
config = {
"LogicDifficulty": world.difficulty[player].value,
"StartingLocation": world.starting_location[player].value,
"LogicDifficulty": options.difficulty.value,
"StartingLocation": options.starting_location.value,
"VersionCreated": "AP",
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
"AllowHints": bool(world.corpse_hints[player].value),
"AllowPenitence": bool(world.penitence[player].value),
"UnlockTeleportation": bool(options.prie_dieu_warp.value),
"AllowHints": bool(options.corpse_hints.value),
"AllowPenitence": bool(options.penitence.value),
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
"ShuffleDash": bool(world.dash_shuffle[player].value),
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
"ShuffleReliquaries": bool(options.reliquary_shuffle.value),
"ShuffleBootsOfPleading": bool(options.boots_of_pleading.value),
"ShufflePurifiedHand": bool(options.purified_hand.value),
"ShuffleDash": bool(options.dash_shuffle.value),
"ShuffleWallClimb": bool(options.wall_climb_shuffle.value),
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
"ShuffleSwordSkills": bool(options.skill_randomizer.value),
"ShuffleThorns": thorns,
"JunkLongQuests": bool(world.skip_long_quests[player].value),
"StartWithWheel": bool(world.start_wheel[player].value),
"JunkLongQuests": bool(options.skip_long_quests.value),
"StartWithWheel": bool(options.start_wheel.value),
"EnemyShuffleType": world.enemy_randomizer[player].value,
"MaintainClass": bool(world.enemy_groups[player].value),
"AreaScaling": bool(world.enemy_scaling[player].value),
"EnemyShuffleType": options.enemy_randomizer.value,
"MaintainClass": bool(options.enemy_groups.value),
"AreaScaling": bool(options.enemy_scaling.value),
"BossShuffleType": 0,
"DoorShuffleType": 0
@@ -385,8 +390,8 @@ class BlasphemousWorld(World):
"locations": locations,
"doors": doors,
"cfg": config,
"ending": world.ending[self.player].value,
"death_link": bool(world.death_link[self.player].value)
"ending": options.ending.value,
"death_link": bool(options.death_link.value)
}
return slot_data

View File

@@ -4,7 +4,8 @@
# https://opensource.org/licenses/MIT
import typing
from Options import Option, Range
from Options import Option, Range, PerGameCommonOptions
from dataclasses import dataclass
class TaskAdvances(Range):
@@ -69,12 +70,12 @@ class KillerTrapWeight(Range):
default = 0
bumpstik_options: typing.Dict[str, type(Option)] = {
"task_advances": TaskAdvances,
"turners": Turners,
"paint_cans": PaintCans,
"trap_count": Traps,
"rainbow_trap_weight": RainbowTrapWeight,
"spinner_trap_weight": SpinnerTrapWeight,
"killer_trap_weight": KillerTrapWeight
}
@dataclass
class BumpStikOptions(PerGameCommonOptions):
task_advances: TaskAdvances
turners: Turners
paint_cans: PaintCans
trap_count: Traps
rainbow_trap_weight: RainbowTrapWeight
spinner_trap_weight: SpinnerTrapWeight
killer_trap_weight: KillerTrapWeight

View File

@@ -43,7 +43,8 @@ class BumpStikWorld(World):
required_client_version = (0, 3, 8)
option_definitions = bumpstik_options
options = BumpStikOptions
options_dataclass = BumpStikOptions
def __init__(self, world: MultiWorld, player: int):
super(BumpStikWorld, self).__init__(world, player)
@@ -86,13 +87,13 @@ class BumpStikWorld(World):
return "Nothing"
def generate_early(self):
self.task_advances = self.multiworld.task_advances[self.player].value
self.turners = self.multiworld.turners[self.player].value
self.paint_cans = self.multiworld.paint_cans[self.player].value
self.traps = self.multiworld.trap_count[self.player].value
self.rainbow_trap_weight = self.multiworld.rainbow_trap_weight[self.player].value
self.spinner_trap_weight = self.multiworld.spinner_trap_weight[self.player].value
self.killer_trap_weight = self.multiworld.killer_trap_weight[self.player].value
self.task_advances = self.options.task_advances.value
self.turners = self.options.turners.value
self.paint_cans = self.options.paint_cans.value
self.traps = self.options.trap_count.value
self.rainbow_trap_weight = self.options.rainbow_trap_weight.value
self.spinner_trap_weight = self.options.spinner_trap_weight.value
self.killer_trap_weight = self.options.killer_trap_weight.value
def create_regions(self):
create_regions(self.multiworld, self.player)

View File

@@ -1,6 +1,7 @@
import typing
from Options import Option
from Options import Option, PerGameCommonOptions
from dataclasses import dataclass
checksfinder_options: typing.Dict[str, type(Option)] = {
}
@dataclass
class ChecksFinderOptions(PerGameCommonOptions):
pass

View File

@@ -1,9 +1,10 @@
from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification
from .Items import ChecksFinderItem, item_table, required_items
from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table
from .Options import checksfinder_options
from .Options import ChecksFinderOptions
from .Rules import set_rules, set_completion_rules
from ..AutoWorld import World, WebWorld
from dataclasses import fields
client_version = 7
@@ -26,7 +27,8 @@ class ChecksFinderWorld(World):
with the mines! You win when you get all your items and beat the board!
"""
game: str = "ChecksFinder"
option_definitions = checksfinder_options
options = ChecksFinderOptions
options_dataclass = ChecksFinderOptions
topology_present = True
web = ChecksFinderWeb()
@@ -79,8 +81,8 @@ class ChecksFinderWorld(World):
def fill_slot_data(self):
slot_data = self._get_checksfinder_data()
for option_name in checksfinder_options:
option = getattr(self.multiworld, option_name)[self.player]
for option_name in [field.name for field in fields(ChecksFinderOptions)]:
option = getattr(self.options, option_name)
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
return slot_data

View File

@@ -1,6 +1,7 @@
from typing import Callable, Dict, NamedTuple, Optional
from BaseClasses import Item, ItemClassification, MultiWorld
from BaseClasses import Item, ItemClassification
from .Options import CliqueOptions
class CliqueItem(Item):
@@ -10,7 +11,7 @@ class CliqueItem(Item):
class CliqueItemData(NamedTuple):
code: Optional[int] = None
type: ItemClassification = ItemClassification.filler
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
can_create: Callable[[CliqueOptions], bool] = lambda options: True
item_data_table: Dict[str, CliqueItemData] = {
@@ -21,11 +22,11 @@ item_data_table: Dict[str, CliqueItemData] = {
"Button Activation": CliqueItemData(
code=69696968,
type=ItemClassification.progression,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
can_create=lambda options: bool(getattr(options, "hard_mode")),
),
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
code=69696967,
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
can_create=lambda options: False # Only created from `get_filler_item_name`.
),
"The Urge to Push": CliqueItemData(
type=ItemClassification.progression,

View File

@@ -1,6 +1,8 @@
from typing import Callable, Dict, NamedTuple, Optional
from BaseClasses import Location, MultiWorld
from BaseClasses import Location
from .Options import CliqueOptions
class CliqueLocation(Location):
@@ -10,7 +12,7 @@ class CliqueLocation(Location):
class CliqueLocationData(NamedTuple):
region: str
address: Optional[int] = None
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
can_create: Callable[[CliqueOptions], bool] = lambda options: True
locked_item: Optional[str] = None
@@ -22,7 +24,7 @@ location_data_table: Dict[str, CliqueLocationData] = {
"The Item on the Desk": CliqueLocationData(
region="The Button Realm",
address=69696968,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
can_create=lambda options: bool(getattr(options, "hard_mode")),
),
"In the Player's Mind": CliqueLocationData(
region="The Button Realm",

View File

@@ -1,6 +1,7 @@
from typing import Dict
from Options import Choice, Option, Toggle
from Options import Choice, Option, Toggle, PerGameCommonOptions
from dataclasses import dataclass
class HardMode(Toggle):
@@ -25,10 +26,12 @@ class ButtonColor(Choice):
option_black = 11
clique_options: Dict[str, type(Option)] = {
"color": ButtonColor,
"hard_mode": HardMode,
@dataclass
class CliqueOptions(PerGameCommonOptions):
color: ButtonColor
hard_mode: HardMode
# DeathLink is always on. Always.
# "death_link": DeathLink,
}
# death_link: DeathLink

View File

@@ -1,10 +1,11 @@
from typing import Callable
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState
from .Options import CliqueOptions
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]:
if getattr(multiworld, "hard_mode")[player]:
def get_button_rule(options: CliqueOptions, player: int) -> Callable[[CollectionState], bool]:
if getattr(options, "hard_mode"):
return lambda state: state.has("Button Activation", player)
return lambda state: True

View File

@@ -4,7 +4,7 @@ from BaseClasses import Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import CliqueItem, item_data_table, item_table
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
from .Options import clique_options
from .Options import CliqueOptions
from .Regions import region_data_table
from .Rules import get_button_rule
@@ -29,7 +29,8 @@ class CliqueWorld(World):
game = "Clique"
data_version = 3
web = CliqueWebWorld()
option_definitions = clique_options
options = CliqueOptions
options_dataclass = CliqueOptions
location_name_to_id = location_table
item_name_to_id = item_table
@@ -39,7 +40,7 @@ class CliqueWorld(World):
def create_items(self) -> None:
item_pool: List[CliqueItem] = []
for name, item in item_data_table.items():
if item.code and item.can_create(self.multiworld, self.player):
if item.code and item.can_create(self.options):
item_pool.append(self.create_item(name))
self.multiworld.itempool += item_pool
@@ -55,27 +56,27 @@ class CliqueWorld(World):
region = self.multiworld.get_region(region_name, self.player)
region.add_locations({
location_name: location_data.address for location_name, location_data in location_data_table.items()
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player)
if location_data.region == region_name and location_data.can_create(self.options)
}, CliqueLocation)
region.add_exits(region_data_table[region_name].connecting_regions)
# Place locked locations.
for location_name, location_data in locked_locations.items():
# Ignore locations we never created.
if not location_data.can_create(self.multiworld, self.player):
if not location_data.can_create(self.options):
continue
locked_item = self.create_item(location_data_table[location_name].locked_item)
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
# Set priority location for the Big Red Button!
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
self.options.priority_locations.value.add("The Big Red Button")
def get_filler_item_name(self) -> str:
return "A Cool Filler Item (No Satisfaction Guaranteed)"
def set_rules(self) -> None:
button_rule = get_button_rule(self.multiworld, self.player)
button_rule = get_button_rule(self.options, self.player)
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
@@ -88,5 +89,5 @@ class CliqueWorld(World):
def fill_slot_data(self):
return {
"color": getattr(self.multiworld, "color")[self.player].current_key
"color": getattr(self.options, "color").current_key
}

View File

@@ -1,6 +1,7 @@
import typing
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink, PerGameCommonOptions
from dataclasses import dataclass
class RandomizeWeaponLocations(DefaultOnToggle):
@@ -200,36 +201,36 @@ class EnableDLCOption(Toggle):
display_name = "Enable DLC"
dark_souls_options: typing.Dict[str, Option] = {
"enable_weapon_locations": RandomizeWeaponLocations,
"enable_shield_locations": RandomizeShieldLocations,
"enable_armor_locations": RandomizeArmorLocations,
"enable_ring_locations": RandomizeRingLocations,
"enable_spell_locations": RandomizeSpellLocations,
"enable_key_locations": RandomizeKeyLocations,
"enable_boss_locations": RandomizeBossSoulLocations,
"enable_npc_locations": RandomizeNPCLocations,
"enable_misc_locations": RandomizeMiscLocations,
"enable_health_upgrade_locations": RandomizeHealthLocations,
"enable_progressive_locations": RandomizeProgressiveLocationsOption,
"pool_type": PoolTypeOption,
"guaranteed_items": GuaranteedItemsOption,
"auto_equip": AutoEquipOption,
"lock_equip": LockEquipOption,
"no_weapon_requirements": NoWeaponRequirementsOption,
"randomize_infusion": RandomizeInfusionOption,
"randomize_infusion_percentage": RandomizeInfusionPercentageOption,
"randomize_weapon_level": RandomizeWeaponLevelOption,
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption,
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
"early_banner": EarlySmallLothricBanner,
"late_basin_of_vows": LateBasinOfVowsOption,
"late_dlc": LateDLCOption,
"no_spell_requirements": NoSpellRequirementsOption,
"no_equip_load": NoEquipLoadOption,
"death_link": DeathLink,
"enable_dlc": EnableDLCOption,
}
@dataclass
class DarkSouls3Options(PerGameCommonOptions):
enable_weapon_locations: RandomizeWeaponLocations
enable_shield_locations: RandomizeShieldLocations
enable_armor_locations: RandomizeArmorLocations
enable_ring_locations: RandomizeRingLocations
enable_spell_locations: RandomizeSpellLocations
enable_key_locations: RandomizeKeyLocations
enable_boss_locations: RandomizeBossSoulLocations
enable_npc_locations: RandomizeNPCLocations
enable_misc_locations: RandomizeMiscLocations
enable_health_upgrade_locations: RandomizeHealthLocations
enable_progressive_locations: RandomizeProgressiveLocationsOption
pool_type: PoolTypeOption
guaranteed_items: GuaranteedItemsOption
auto_equip: AutoEquipOption
lock_equip: LockEquipOption
no_weapon_requirements: NoWeaponRequirementsOption
randomize_infusion: RandomizeInfusionOption
randomize_infusion_percentage: RandomizeInfusionPercentageOption
randomize_weapon_level: RandomizeWeaponLevelOption
randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption
min_levels_in_5: MinLevelsIn5WeaponPoolOption
max_levels_in_5: MaxLevelsIn5WeaponPoolOption
min_levels_in_10: MinLevelsIn10WeaponPoolOption
max_levels_in_10: MaxLevelsIn10WeaponPoolOption
early_banner: EarlySmallLothricBanner
late_basin_of_vows: LateBasinOfVowsOption
late_dlc: LateDLCOption
no_spell_requirements: NoSpellRequirementsOption
no_equip_load: NoEquipLoadOption
death_link: DeathLink
enable_dlc: EnableDLCOption

View File

@@ -9,7 +9,7 @@ from worlds.generic.Rules import set_rule, add_rule, add_item_rule
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, DarkSouls3Options
class DarkSouls3Web(WebWorld):
@@ -43,7 +43,8 @@ class DarkSouls3World(World):
"""
game: str = "Dark Souls III"
option_definitions = dark_souls_options
options = DarkSouls3Options
options_dataclass = DarkSouls3Options
topology_present: bool = True
web = DarkSouls3Web()
data_version = 8
@@ -72,47 +73,47 @@ class DarkSouls3World(World):
def generate_early(self):
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
if self.options.enable_weapon_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.WEAPON)
if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true:
if self.options.enable_shield_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.SHIELD)
if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true:
if self.options.enable_armor_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.ARMOR)
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
if self.options.enable_ring_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.RING)
if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true:
if self.options.enable_spell_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.SPELL)
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
if self.options.enable_npc_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.NPC)
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
if self.options.enable_key_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.KEY)
if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global:
self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1
elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local:
self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
if self.options.early_banner == EarlySmallLothricBanner.option_early_global:
self.options.early_items['Small Lothric Banner'] = 1
elif self.options.early_banner == EarlySmallLothricBanner.option_early_local:
self.options.local_early_items['Small Lothric Banner'] = 1
if self.options.enable_boss_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
if self.options.enable_misc_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.MISC)
if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true:
if self.options.enable_health_upgrade_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.HEALTH)
if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true:
if self.options.enable_progressive_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM)
def create_regions(self):
progressive_location_table = []
if self.multiworld.enable_progressive_locations[self.player]:
if self.options.enable_progressive_locations:
progressive_location_table = [] + \
location_tables["Progressive Items 1"] + \
location_tables["Progressive Items 2"] + \
location_tables["Progressive Items 3"] + \
location_tables["Progressive Items 4"]
if self.multiworld.enable_dlc[self.player].value:
if self.options.enable_dlc.value:
progressive_location_table += location_tables["Progressive Items DLC"]
if self.multiworld.enable_health_upgrade_locations[self.player]:
if self.options.enable_health_upgrade_locations:
progressive_location_table += location_tables["Progressive Items Health"]
# Create Vanilla Regions
@@ -146,7 +147,7 @@ class DarkSouls3World(World):
regions["Consumed King's Garden"].locations.append(potd_location)
# Create DLC Regions
if self.multiworld.enable_dlc[self.player]:
if self.options.enable_dlc:
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
"Painted World of Ariandel 1",
"Painted World of Ariandel 2",
@@ -192,7 +193,7 @@ class DarkSouls3World(World):
create_connection("Consumed King's Garden", "Untended Graves")
# Connect DLC Regions
if self.multiworld.enable_dlc[self.player]:
if self.options.enable_dlc:
create_connection("Cathedral of the Deep", "Painted World of Ariandel 1")
create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2")
create_connection("Painted World of Ariandel 2", "Dreg Heap")
@@ -240,7 +241,7 @@ class DarkSouls3World(World):
def create_items(self):
dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true
dlc_enabled = self.options.enable_dlc == Toggle.option_true
itempool_by_category = {category: [] for category in self.enabled_location_categories}
@@ -254,7 +255,7 @@ class DarkSouls3World(World):
itempool_by_category[location.category].append(location.default_item_name)
# Replace each item category with a random sample of items of those types
if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various:
if self.options.pool_type == PoolTypeOption.option_various:
def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int):
candidates = [
item.name for item
@@ -300,7 +301,7 @@ class DarkSouls3World(World):
# A list of items we can replace
removable_items = [item for item in itempool if item.classification != ItemClassification.progression]
guaranteed_items = self.multiworld.guaranteed_items[self.player].value
guaranteed_items = self.options.guaranteed_items.value
for item_name in guaranteed_items:
# Break early just in case nothing is removable (if user is trying to guarantee more
# items than the pool can hold, for example)
@@ -384,22 +385,22 @@ class DarkSouls3World(World):
state.has("Cinders of a Lord - Aldrich", self.player) and
state.has("Cinders of a Lord - Lothric Prince", self.player))
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
if self.options.late_basin_of_vows == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
# DLC Access Rules Below
if self.multiworld.enable_dlc[self.player]:
if self.options.enable_dlc:
set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player),
lambda state: state.has("Small Envoy Banner", self.player))
# If key items are randomized, must have contraption key to enter second half of Ashes DLC
# If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
if self.options.enable_key_locations == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player),
lambda state: state.has("Contraption Key", self.player))
if self.multiworld.late_dlc[self.player] == Toggle.option_true:
if self.options.late_dlc == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player),
lambda state: state.has("Small Doll", self.player))
@@ -407,7 +408,7 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player),
lambda state: state.has("Storm Ruler", self.player))
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
if self.options.enable_ring_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
lambda state: state.has("Jailbreaker's Key", self.player))
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
@@ -415,7 +416,7 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
if self.options.enable_npc_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
@@ -431,11 +432,11 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
if self.options.enable_misc_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
if self.options.enable_boss_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player),
lambda state: state.has("Storm Ruler", self.player))
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
@@ -443,7 +444,7 @@ class DarkSouls3World(World):
# Lump Soul of the Dancer in with LC for locations that should not be reachable
# before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US)
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
if self.options.late_basin_of_vows == Toggle.option_true:
add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
@@ -453,10 +454,10 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule)
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
if self.options.enable_weapon_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule)
self.multiworld.completion_condition[self.player] = lambda state: \
self.options.completion_condition = lambda state: \
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \
state.has("Cinders of a Lord - Aldrich", self.player) and \
@@ -470,13 +471,13 @@ class DarkSouls3World(World):
name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()}
# Randomize some weapon upgrades
if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none:
if self.options.randomize_weapon_level != RandomizeWeaponLevelOption.option_none:
# if the user made an error and set a min higher than the max we default to the max
max_5 = self.multiworld.max_levels_in_5[self.player]
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5)
max_10 = self.multiworld.max_levels_in_10[self.player]
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10)
weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player]
max_5 = self.options.max_levels_in_5
min_5 = min(self.options.min_levels_in_5, max_5)
max_10 = self.options.max_levels_in_10
min_10 = min(self.options.min_levels_in_10, max_10)
weapon_level_percentage = self.options.randomize_weapon_level_percentage
for item in item_dictionary.values():
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage:
@@ -486,8 +487,8 @@ class DarkSouls3World(World):
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
# Randomize some weapon infusions
if self.multiworld.randomize_infusion[self.player] == Toggle.option_true:
infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player]
if self.options.randomize_infusion == Toggle.option_true:
infusion_percentage = self.options.randomize_infusion_percentage
for item in item_dictionary.values():
if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}:
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage:
@@ -518,22 +519,22 @@ class DarkSouls3World(World):
slot_data = {
"options": {
"enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value,
"enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value,
"enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value,
"enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value,
"enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value,
"enable_key_locations": self.multiworld.enable_key_locations[self.player].value,
"enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value,
"enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value,
"enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value,
"auto_equip": self.multiworld.auto_equip[self.player].value,
"lock_equip": self.multiworld.lock_equip[self.player].value,
"no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value,
"death_link": self.multiworld.death_link[self.player].value,
"no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value,
"no_equip_load": self.multiworld.no_equip_load[self.player].value,
"enable_dlc": self.multiworld.enable_dlc[self.player].value
"enable_weapon_locations": self.options.enable_weapon_locations.value,
"enable_shield_locations": self.options.enable_shield_locations.value,
"enable_armor_locations": self.options.enable_armor_locations.value,
"enable_ring_locations": self.options.enable_ring_locations.value,
"enable_spell_locations": self.options.enable_spell_locations.value,
"enable_key_locations": self.options.enable_key_locations.value,
"enable_boss_locations": self.options.enable_boss_locations.value,
"enable_npc_locations": self.options.enable_npc_locations.value,
"enable_misc_locations": self.options.enable_misc_locations.value,
"auto_equip": self.options.auto_equip.value,
"lock_equip": self.options.lock_equip.value,
"no_weapon_requirements": self.options.no_weapon_requirements.value,
"death_link": self.options.death_link.value,
"no_spell_requirements": self.options.no_spell_requirements.value,
"no_equip_load": self.options.no_equip_load.value,
"enable_dlc": self.options.enable_dlc.value
},
"seed": self.multiworld.seed_name, # to verify the server's multiworld
"slot": self.multiworld.player_name[self.player], # to connect to server

View File

@@ -6,6 +6,8 @@ import shutil
import threading
import zipfile
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
from dataclasses import fields
import datetime
import jinja2
@@ -88,6 +90,7 @@ class FactorioModFile(worlds.Files.APContainer):
def generate_mod(world: "Factorio", output_directory: str):
player = world.player
multiworld = world.multiworld
options = world.options
global data_final_template, locale_template, control_template, data_template, settings_template
with template_load_lock:
if not data_final_template:
@@ -129,40 +132,40 @@ def generate_mod(world: "Factorio", output_directory: str):
"base_tech_table": base_tech_table,
"tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name,
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"allowed_science_packs": options.max_science_pack.get_allowed_packs(),
"custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"slot_player": player,
"starting_items": multiworld.starting_items[player], "recipes": recipes,
"starting_items": options.starting_items, "recipes": recipes,
"random": random, "flop_random": flop_random,
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
"recipe_time_scale": recipe_time_scales.get(options.recipe_time.value, None),
"recipe_time_range": recipe_time_ranges.get(options.recipe_time.value, None),
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
"progressive_technology_table": {tech.name: tech.progressive for tech in
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value,
"max_science_pack": options.max_science_pack.value,
"liquids": fluids,
"goal": multiworld.goal[player].value,
"energy_link": multiworld.energy_link[player].value,
"goal": options.goal.value,
"energy_link": options.energy_link.value,
"useless_technologies": useless_technologies,
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
"chunk_shuffle": options.chunk_shuffle.value if datetime.datetime.today().month == 4 else 0,
}
for factorio_option in Options.factorio_options:
for factorio_option in [field.name for field in fields(Options.FactorioOptions)]:
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
continue
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
template_data[factorio_option] = getattr(options, factorio_option).value
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
if getattr(options, "silo").value == Options.Silo.option_randomize_recipe:
template_data["free_sample_blacklist"]["rocket-silo"] = 1
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
if getattr(options, "satellite").value == Options.Satellite.option_randomize_recipe:
template_data["free_sample_blacklist"]["satellite"] = 1
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
template_data["free_sample_blacklist"].update({item: 1 for item in options.free_sample_blacklist.value})
template_data["free_sample_blacklist"].update({item: 0 for item in options.free_sample_whitelist.value})
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
@@ -177,7 +180,7 @@ def generate_mod(world: "Factorio", output_directory: str):
else:
basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
for dirpath, dirnames, filenames in os.walk(basepath):
base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)
base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\")
for filename in filenames:
mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
file_path=os.path.join(dirpath, filename):

View File

@@ -1,10 +1,10 @@
from __future__ import annotations
import typing
import datetime
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool
StartInventoryPool, PerGameCommonOptions
from schema import Schema, Optional, And, Or
from dataclasses import dataclass
# schema helpers
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
@@ -422,50 +422,44 @@ class EnergyLink(Toggle):
display_name = "EnergyLink"
factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack,
"goal": Goal,
"tech_tree_layout": TechTreeLayout,
"min_tech_cost": MinTechCost,
"max_tech_cost": MaxTechCost,
"tech_cost_distribution": TechCostDistribution,
"tech_cost_mix": TechCostMix,
"ramping_tech_costs": RampingTechCosts,
"silo": Silo,
"satellite": Satellite,
"free_samples": FreeSamples,
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"free_sample_blacklist": FactorioFreeSampleBlacklist,
"free_sample_whitelist": FactorioFreeSampleWhitelist,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"recipe_ingredients_offset": RecipeIngredientsOffset,
"imported_blueprints": ImportedBlueprint,
"world_gen": FactorioWorldGen,
"progressive": Progressive,
"teleport_traps": TeleportTrapCount,
"grenade_traps": GrenadeTrapCount,
"cluster_grenade_traps": ClusterGrenadeTrapCount,
"artillery_traps": ArtilleryTrapCount,
"atomic_rocket_traps": AtomicRocketTrapCount,
"attack_traps": AttackTrapCount,
"evolution_traps": EvolutionTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
"death_link": DeathLink,
"energy_link": EnergyLink,
"start_inventory_from_pool": StartInventoryPool,
}
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
if datetime.datetime.today().month == 4:
class ChunkShuffle(Toggle):
"""Entrance Randomizer."""
display_name = "Chunk Shuffle"
class ChunkShuffle(Toggle):
"""Entrance Randomizer.
2023 April Fool's option. Shuffles chunk border transitions.
Only valid during the Month of April. Forced off otherwise."""
if datetime.datetime.today().day > 1:
ChunkShuffle.__doc__ += """
2023 April Fool's option. Shuffles chunk border transitions."""
factorio_options["chunk_shuffle"] = ChunkShuffle
@dataclass
class FactorioOptions(PerGameCommonOptions):
max_science_pack: MaxSciencePack
goal: Goal
tech_tree_layout: TechTreeLayout
min_tech_cost: MinTechCost
max_tech_cost: MaxTechCost
tech_cost_distribution: TechCostDistribution
tech_cost_mix: TechCostMix
ramping_tech_costs: RampingTechCosts
silo: Silo
satellite: Satellite
free_samples: FreeSamples
tech_tree_information: TechTreeInformation
starting_items: FactorioStartItems
free_sample_blacklist: FactorioFreeSampleBlacklist
free_sample_whitelist: FactorioFreeSampleWhitelist
recipe_time: RecipeTime
recipe_ingredients: RecipeIngredients
recipe_ingredients_offset: RecipeIngredientsOffset
imported_blueprints: ImportedBlueprint
world_gen: FactorioWorldGen
progressive: Progressive
teleport_traps: TeleportTrapCount
grenade_traps: GrenadeTrapCount
cluster_grenade_traps: ClusterGrenadeTrapCount
artillery_traps: ArtilleryTrapCount
atomic_rocket_traps: AtomicRocketTrapCount
attack_traps: AttackTrapCount
evolution_traps: EvolutionTrapCount
evolution_trap_increase: EvolutionTrapIncrease
death_link: DeathLink
energy_link: EnergyLink
start_inventory_from_pool: StartInventoryPool
chunk_shuffle: ChunkShuffle

View File

@@ -20,10 +20,10 @@ def _sorter(location: "FactorioScienceLocation"):
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
options = factorio_world.options
world = factorio_world.multiworld
player = factorio_world.player
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
layout = world.tech_tree_layout[player].value
layout = options.tech_tree_layout.value
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
world.random.shuffle(locations)

View File

@@ -11,7 +11,7 @@ from worlds.LauncherComponents import Component, components, Type, launch_subpro
from worlds.generic import Rules
from .Locations import location_pools, location_table
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution, TechCostMix
from .Shapes import get_shapes
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
@@ -88,6 +88,9 @@ class Factorio(World):
location_pool: typing.List[FactorioScienceLocation]
advancement_technologies: typing.Set[str]
options = FactorioOptions
options_dataclass = FactorioOptions
web = FactorioWeb()
item_name_to_id = all_items
@@ -117,11 +120,11 @@ class Factorio(World):
def generate_early(self) -> None:
# if max < min, then swap max and min
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
if self.options.max_tech_cost < self.options.min_tech_cost:
self.options.min_tech_cost.value, self.options.max_tech_cost.value = \
self.options.max_tech_cost.value, self.options.min_tech_cost.value
self.tech_mix = self.options.tech_cost_mix
self.skip_silo = self.options.silo.value == Silo.option_spawn
def create_regions(self):
player = self.player
@@ -132,17 +135,17 @@ class Factorio(World):
nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.multiworld.evolution_traps[player] + \
self.multiworld.attack_traps[player] + \
self.multiworld.teleport_traps[player] + \
self.multiworld.grenade_traps[player] + \
self.multiworld.cluster_grenade_traps[player] + \
self.multiworld.atomic_rocket_traps[player] + \
self.multiworld.artillery_traps[player]
self.options.evolution_traps + \
self.options.attack_traps + \
self.options.teleport_traps + \
self.options.grenade_traps + \
self.options.cluster_grenade_traps + \
self.options.atomic_rocket_traps + \
self.options.artillery_traps
location_pool = []
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
location_pool.extend(location_pools[pack])
try:
location_names = self.multiworld.random.sample(location_pool, location_count)
@@ -151,11 +154,11 @@ class Factorio(World):
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
f"or increase the location count (higher max science pack). (Player {self.player})") from e
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis, self.options.tech_cost_mix)
for loc_name in location_names]
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
min_cost = self.multiworld.min_tech_cost[self.player]
max_cost = self.multiworld.max_tech_cost[self.player]
distribution: TechCostDistribution = self.options.tech_cost_distribution
min_cost = self.options.min_tech_cost
max_cost = self.options.max_tech_cost
if distribution == distribution.option_even:
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
else:
@@ -164,7 +167,7 @@ class Factorio(World):
distribution.option_high: max_cost}[distribution.value]
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
rand_values = sorted(rand_values)
if self.multiworld.ramping_tech_costs[self.player]:
if self.options.ramping_tech_costs:
def sorter(loc: FactorioScienceLocation):
return loc.complexity, loc.rel_cost
else:
@@ -179,7 +182,7 @@ class Factorio(World):
event = FactorioItem("Victory", ItemClassification.progression, None, player)
location.place_locked_item(event)
for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()):
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
nauvis.locations.append(location)
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
@@ -195,10 +198,10 @@ class Factorio(World):
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.multiworld,
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
range(getattr(self.options,
f"{trap_name.lower().replace(' ', '_')}_traps")))
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
want_progressives = collections.defaultdict(lambda: self.options.progressive.
want_progressives(self.multiworld.random))
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
@@ -206,7 +209,7 @@ class Factorio(World):
"logistics": 1,
"rocket-silo": -1}
loc: FactorioScienceLocation
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full:
if self.options.tech_tree_information == TechTreeInformation.option_full:
# mark all locations as pre-hinted
for loc in self.science_locations:
loc.revealed = True
@@ -237,16 +240,17 @@ class Factorio(World):
player = self.player
shapes = get_shapes(self)
for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
for ingredient in self.options.max_science_pack.get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player)
if self.multiworld.recipe_ingredients[self.player]:
if self.options.recipe_ingredients:
custom_recipe = self.custom_recipes[ingredient]
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
(ingredient not in technology_table or state.has(ingredient, player)) and \
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
for technology in required_technologies[sub_ingredient])
for technology in required_technologies[sub_ingredient]) and \
all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine])
else:
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
@@ -260,16 +264,16 @@ class Factorio(World):
prerequisites: all(state.can_reach(loc) for loc in locations))
silo_recipe = None
if self.multiworld.silo[self.player] == Silo.option_spawn:
if self.options.silo == Silo.option_spawn:
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None
if self.multiworld.goal[self.player] == Goal.option_satellite:
if self.options.goal == Goal.option_satellite:
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
else next(iter(all_product_sources.get("satellite")))
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
if self.multiworld.silo[self.player] != Silo.option_spawn:
if self.options.silo != Silo.option_spawn:
victory_tech_names.add("rocket-silo")
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
@@ -278,12 +282,12 @@ class Factorio(World):
world.completion_condition[player] = lambda state: state.has('Victory', player)
def generate_basic(self):
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
map_basic_settings = self.options.world_gen.value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0
# 32 bit uint
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)
start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value
start_location_hints: typing.Set[str] = self.options.start_location_hints.value
for loc in self.science_locations:
# show start_location_hints ingame
@@ -307,8 +311,6 @@ class Factorio(World):
return super(Factorio, self).collect_item(state, item, remove)
option_definitions = factorio_options
@classmethod
def stage_write_spoiler(cls, world, spoiler_handle):
factorio_players = world.get_game_players(cls.game)
@@ -436,25 +438,25 @@ class Factorio(World):
def set_custom_technologies(self):
custom_technologies = {}
allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs()
allowed_packs = self.options.max_science_pack.get_allowed_packs()
for technology_name, technology in base_technology_table.items():
custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player)
return custom_technologies
def set_custom_recipes(self):
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
ingredients_offset = self.options.recipe_ingredients_offset
original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients)
self.multiworld.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
original_rocket_part.products,
original_rocket_part.energy)}
if self.multiworld.recipe_ingredients[self.player]:
if self.options.recipe_ingredients:
valid_pool = []
for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs():
for pack in self.options.max_science_pack.get_ordered_science_packs():
valid_pool += sorted(science_pack_pools[pack])
self.multiworld.random.shuffle(valid_pool)
if pack in recipes: # skips over space science pack
@@ -462,23 +464,23 @@ class Factorio(World):
ingredients_offset)
self.custom_recipes[pack] = new_recipe
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
if self.options.silo.value == Silo.option_randomize_recipe \
or self.options.satellite.value == Satellite.option_randomize_recipe:
valid_pool = set()
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
valid_pool |= science_pack_pools[pack]
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
if self.options.silo.value == Silo.option_randomize_recipe:
new_recipe = self.make_balanced_recipe(
recipes["rocket-silo"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
factor=(self.options.max_science_pack.value + 1) / 7,
ingredients_offset=ingredients_offset)
self.custom_recipes["rocket-silo"] = new_recipe
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
if self.options.satellite.value == Satellite.option_randomize_recipe:
new_recipe = self.make_balanced_recipe(
recipes["satellite"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
factor=(self.options.max_science_pack.value + 1) / 7,
ingredients_offset=ingredients_offset)
self.custom_recipes["satellite"] = new_recipe
bridge = "ap-energy-bridge"
@@ -486,16 +488,16 @@ class Factorio(World):
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
"replace_4": 1, "replace_5": 1, "replace_6": 1},
{bridge: 1}, 10),
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]),
ingredients_offset=ingredients_offset)
for ingredient_name in new_recipe.ingredients:
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
self.custom_recipes[bridge] = new_recipe
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
if self.multiworld.silo[self.player] != Silo.option_spawn:
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
if self.options.silo != Silo.option_spawn:
needed_recipes |= {"rocket-silo"}
if self.multiworld.goal[self.player].value == Goal.option_satellite:
if self.options.goal.value == Goal.option_satellite:
needed_recipes |= {"satellite"}
for recipe in needed_recipes:
@@ -537,7 +539,7 @@ class FactorioScienceLocation(FactorioLocation):
ingredients: typing.Dict[str, int]
count: int = 0
def __init__(self, player: int, name: str, address: int, parent: Region):
def __init__(self, player: int, name: str, address: int, parent: Region, tech_cost_mix: TechCostMix):
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
# "AP-{Complexity}-{Cost}"
self.complexity = int(self.name[3]) - 1
@@ -545,7 +547,7 @@ class FactorioScienceLocation(FactorioLocation):
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
for complexity in range(self.complexity):
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
if tech_cost_mix > parent.multiworld.random.randint(0, 99):
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
@property

View File

@@ -15,9 +15,9 @@ else:
def locality_needed(world: MultiWorld) -> bool:
for player in world.player_ids:
if world.local_items[player].value:
if world.worlds[player].options.local_items.value:
return True
if world.non_local_items[player].value:
if world.worlds[player].options.non_local_items.value:
return True
# Group
@@ -40,12 +40,12 @@ def locality_rules(world: MultiWorld):
forbid_data[sender][receiver].update(items)
for receiving_player in world.player_ids:
local_items: typing.Set[str] = world.local_items[receiving_player].value
local_items: typing.Set[str] = world.worlds[receiving_player].options.local_items.value
if local_items:
for sending_player in world.player_ids:
if receiving_player != sending_player:
forbid(sending_player, receiving_player, local_items)
non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value
non_local_items: typing.Set[str] = world.worlds[receiving_player].options.non_local_items.value
if non_local_items:
forbid(receiving_player, receiving_player, non_local_items)

View File

@@ -2,7 +2,7 @@ import typing
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink
from .Charms import vanilla_costs, names as charm_names
if typing.TYPE_CHECKING:
@@ -402,22 +402,34 @@ class WhitePalace(Choice):
default = 0
class DeathLink(Choice):
class ExtraPlatforms(DefaultOnToggle):
"""Places additional platforms to make traveling throughout Hallownest more convenient."""
class DeathLinkShade(Choice):
"""Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any.
vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any.
shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched.
shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless.
* This option has no effect if DeathLink is disabled.
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
your existing shade, if any.
"""
When you die, everyone dies. Of course the reverse is true too.
When enabled, choose how incoming deathlinks are handled:
vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
"""
option_off = 0
alias_no = 0
alias_true = 1
alias_on = 1
alias_yes = 1
option_vanilla = 0
option_shadeless = 1
option_vanilla = 2
option_shade = 3
option_shade = 2
default = 2
class DeathLinkBreaksFragileCharms(Toggle):
"""Sets if fragile charms break when you are killed by a DeathLink.
* This option has no effect if DeathLink is disabled.
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
will continue to do so.
"""
class StartingGeo(Range):
@@ -476,7 +488,8 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
**{
option.__name__: option
for option in (
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo,
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice,
MinimumEssencePrice, MaximumEssencePrice,
@@ -488,7 +501,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
LegEaterShopSlots, GrubfatherRewardSlots,
SeerRewardSlots, ExtraShopSlots,
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
CostSanity, CostSanityHybridChance,
CostSanity, CostSanityHybridChance
)
},
**cost_sanity_weights

View File

@@ -11,7 +11,6 @@ from .options import LingoOptions
from .player_logic import LingoPlayerLogic
from .regions import create_regions
from .static_logic import Room, RoomEntrance
from .testing import LingoTestOptions
class LingoWebWorld(WebWorld):

View File

@@ -6,7 +6,6 @@ from .options import LocationChecks, ShuffleDoors, VictoryCondition
from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \
RoomAndPanel
from .testing import LingoTestOptions
if TYPE_CHECKING:
from . import LingoWorld
@@ -224,7 +223,7 @@ class LingoPlayerLogic:
"kind of logic error.")
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False:
and not early_color_hallways is False:
# If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
# but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
# now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are

View File

@@ -8,6 +8,8 @@ class TestRequiredRoomLogic(LingoTestBase):
}
def test_pilgrim_first(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
@@ -28,6 +30,8 @@ class TestRequiredRoomLogic(LingoTestBase):
self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
def test_hidden_first(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
@@ -55,6 +59,8 @@ class TestRequiredDoorLogic(LingoTestBase):
}
def test_through_rhyme(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
self.collect_by_name("Starting Room - Rhyme Room Entrance")
@@ -64,6 +70,8 @@ class TestRequiredDoorLogic(LingoTestBase):
self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
def test_through_hidden(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
self.collect_by_name("Starting Room - Rhyme Room Entrance")
@@ -83,6 +91,8 @@ class TestSimpleDoors(LingoTestBase):
}
def test_requirement(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))

View File

@@ -8,6 +8,8 @@ class TestProgressiveOrangeTower(LingoTestBase):
}
def test_from_welcome_back(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
@@ -83,6 +85,8 @@ class TestProgressiveOrangeTower(LingoTestBase):
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
def test_from_hub_room(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))

View File

@@ -7,6 +7,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase):
}
def test_item(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
@@ -58,6 +60,8 @@ class TestSimpleHallwayRoom(LingoTestBase):
}
def test_item(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
@@ -86,6 +90,8 @@ class TestProgressiveArtGallery(LingoTestBase):
}
def test_item(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))

View File

@@ -1,7 +1,6 @@
from typing import ClassVar
from test.bases import WorldTestBase
from .. import LingoTestOptions
class LingoTestBase(WorldTestBase):
@@ -9,5 +8,10 @@ class LingoTestBase(WorldTestBase):
player: ClassVar[int] = 1
def world_setup(self, *args, **kwargs):
LingoTestOptions.disable_forced_good_item = True
super().world_setup(*args, **kwargs)
def remove_forced_good_item(self):
location = self.multiworld.get_location("Second Room - Good Luck", self.player)
self.remove(location.item)
self.multiworld.itempool.append(location.item)
self.multiworld.state.events.add(location)

View File

@@ -1,2 +0,0 @@
class LingoTestOptions:
disable_forced_good_item: bool = False

View File

@@ -414,7 +414,7 @@ class PokemonRedBlueWorld(World):
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player]
and (((self.multiworld.accessibility[self.player] != "minimal" and
and (((self.multiworld.accessibility[self.player] != "minimal" or
(self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or
self.multiworld.door_shuffle[self.player]))):
intervene_move = "Flash"