costs {ctx.hint_cost}% of checks made. " \
+ f"For you this means every " \
+ f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
+ "location checks." \
+ f"\nYou currently have {ctx.hint_points} points."
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
text += "\nRace mode is enabled." \
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
diff --git a/rule_builder/rules.py b/rule_builder/rules.py
index 07c0607c1fb3..0606225ffa03 100644
--- a/rule_builder/rules.py
+++ b/rule_builder/rules.py
@@ -36,7 +36,7 @@ def hash_impl(self: "Rule.Resolved") -> int:
class CustomRuleRegister(type):
"""A metaclass to contain world custom rules and automatically convert resolved rules to frozen dataclasses"""
- resolved_rules: ClassVar[dict[int, "Rule.Resolved"]] = {}
+ resolved_rules: ClassVar[dict["Rule.Resolved", "Rule.Resolved"]] = {}
"""A cached of resolved rules to turn each unique one into a singleton"""
custom_rules: ClassVar[dict[str, dict[str, type["Rule[Any]"]]]] = {}
@@ -64,10 +64,9 @@ def __new__(
@override
def __call__(cls, *args: Any, **kwds: Any) -> Any:
rule = super().__call__(*args, **kwds)
- rule_hash = hash(rule)
- if rule_hash in cls.resolved_rules:
- return cls.resolved_rules[rule_hash]
- cls.resolved_rules[rule_hash] = rule
+ if rule in cls.resolved_rules:
+ return cls.resolved_rules[rule]
+ cls.resolved_rules[rule] = rule
return rule
@classmethod
@@ -426,13 +425,142 @@ def entrance_dependencies(self) -> dict[str, set[int]]:
return combined_deps
+class AtLeast(NestedRule[TWorld], game="Archipelago"):
+ """A rule that returns true when at least N child rules evaluate as true"""
+
+ count: int | FieldResolver
+
+ def __init__(
+ self,
+ count: int | FieldResolver,
+ *children: Rule[TWorld],
+ options: Iterable[OptionFilter] = (),
+ filtered_resolution: bool = False,
+ ) -> None:
+ super().__init__(*children, options=options, filtered_resolution=filtered_resolution)
+ self.count = count
+
+ @override
+ def _instantiate(self, world: TWorld) -> Rule.Resolved:
+ count = resolve_field(self.count, world, int)
+ if count == 0:
+ return True_().resolve(world)
+
+ children_to_process = [c.resolve(world) for c in self.children]
+ return AtLeast.from_resolved(count, world, children_to_process)
+
+ @classmethod
+ def from_resolved(cls, count: int, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
+ clauses: list[Rule.Resolved] = []
+
+ while children_to_process:
+ child = children_to_process.pop(0)
+ if child.always_true:
+ if count == 1:
+ return child
+ count -= 1
+ continue
+ if child.always_false:
+ # falses can be ignored
+ continue
+
+ clauses.append(child)
+
+ if len(clauses) < count:
+ return False_().resolve(world)
+ if count == 1:
+ # Switch to Or which has more optimized handling
+ return Or.from_resolved(world, clauses)
+ if count == len(clauses):
+ # Switch to And which has more optimized handling
+ return And.from_resolved(world, clauses)
+ return AtLeast.Resolved(
+ tuple(clauses),
+ count=count,
+ player=world.player,
+ caching_enabled=getattr(world, "rule_caching_enabled", False),
+ )
+
+ @override
+ def to_dict(self) -> dict[str, Any]:
+ output = super().to_dict()
+ count = self.count
+ output["count"] = count.to_dict() if isinstance(count, FieldResolver) else count
+ return output
+
+ @override
+ @classmethod
+ def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
+ args = cls._parse_field_resolvers(data, world_cls.game)
+ options = OptionFilter.multiple_from_dict(data.get("options", ()))
+ children = [world_cls.rule_from_dict(c) for c in data.get("children", ())]
+ return cls(
+ args.pop("count"),
+ *children,
+ options=options,
+ filtered_resolution=data.get("filtered_resolution", False),
+ )
+
+ class Resolved(NestedRule.Resolved):
+ count: int
+
+ @override
+ def _evaluate(self, state: CollectionState) -> bool:
+ count = self.count
+ for rule in self.children:
+ if rule(state):
+ if count == 1:
+ return True
+ count -= 1
+ return False
+
+ @override
+ def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
+ messages: list[JSONMessagePart] = []
+ if state is None:
+ messages = [
+ {"type": "text", "text": "At least "},
+ {"type": "color", "color": "cyan", "text": str(self.count)},
+ {"type": "text", "text": " of ("},
+ ]
+ else:
+ satisfied_count = sum(1 if child(state) else 0 for child in self.children)
+ messages = [
+ {"type": "text", "text": "At least "},
+ {"type": "color", "color": "cyan", "text": f"{satisfied_count}/{self.count}"},
+ {"type": "text", "text": " of ("},
+ ]
+ for i, child in enumerate(self.children):
+ if i > 0:
+ messages.append({"type": "text", "text": ", "})
+ messages.extend(child.explain_json(state))
+ messages.append({"type": "text", "text": ")"})
+ return messages
+
+ @override
+ def explain_str(self, state: CollectionState | None = None) -> str:
+ clauses = ", ".join([c.explain_str(state) for c in self.children])
+ if state is None:
+ return f"At least {self.count} of ({clauses})"
+ satisfied_count = sum(1 if child(state) else 0 for child in self.children)
+ return f"At least {satisfied_count}/{self.count} of ({clauses})"
+
+ @override
+ def __str__(self) -> str:
+ clauses = ", ".join([str(c) for c in self.children])
+ return f"At least {self.count} of ({clauses})"
+
+
@dataclasses.dataclass(init=False)
class And(NestedRule[TWorld], game="Archipelago"):
"""A rule that only returns true when all child rules evaluate as true"""
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
- children_to_process = [c.resolve(world) for c in self.children]
+ return And.from_resolved(world, [c.resolve(world) for c in self.children])
+
+ @classmethod
+ def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
items: dict[str, int] = {}
true_rule: Rule.Resolved | None = None
@@ -465,7 +593,7 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
clauses.append(child)
if not clauses and not items:
- return true_rule or False_().resolve(world)
+ return true_rule or True_().resolve(world)
if len(items) == 1:
item, count = next(iter(items.items()))
@@ -519,7 +647,10 @@ class Or(NestedRule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
- children_to_process = [c.resolve(world) for c in self.children]
+ return Or.from_resolved(world, [c.resolve(world) for c in self.children])
+
+ @classmethod
+ def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
items: dict[str, int] = {}
@@ -1272,14 +1403,16 @@ def __init__(
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
+ count = resolve_field(self.count, world, int)
+ if count <= 0:
+ return True_().resolve(world)
if len(self.item_names) == 0:
- # match state.has_from_list
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0], self.count).resolve(world)
return self.Resolved(
self.item_names,
- count=resolve_field(self.count, world, int),
+ count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1407,8 +1540,9 @@ def __init__(
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
- if len(self.item_names) == 0 or len(self.item_names) < count:
- # match state.has_from_list_unique
+ if count <= 0:
+ return True_().resolve(world)
+ if len(self.item_names) < count:
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0]).resolve(world)
@@ -1526,11 +1660,14 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
+ count = resolve_field(self.count, world, int)
+ if count <= 0:
+ return True_().resolve(world)
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
return self.Resolved(
self.item_name_group,
item_names,
- count=resolve_field(self.count, world, int),
+ count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1600,11 +1737,16 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
+ count = resolve_field(self.count, world, int)
+ if count <= 0:
+ return True_().resolve(world)
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
+ if len(item_names) < count:
+ return False_().resolve(world)
return self.Resolved(
self.item_name_group,
item_names,
- count=resolve_field(self.count, world, int),
+ count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
diff --git a/settings.py b/settings.py
index 3b3d0deec73f..a23e7aed9d67 100644
--- a/settings.py
+++ b/settings.py
@@ -98,6 +98,8 @@ def __getattribute__(self, item: str) -> Any:
self._changed = True
attr = new
# resolve the path immediately when accessing it
+ if attr.exists():
+ attr.__class__.validate(attr.resolve())
return attr.__class__(attr.resolve())
return attr
@@ -633,10 +635,6 @@ class LogNetwork(IntEnum):
class GeneratorOptions(Group):
"""Options for Generation"""
- class EnemizerPath(LocalFilePath):
- """Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
- is_exe = True
-
class PlayerFilesPath(OptionalUserFolderPath):
"""Folder from which the player yaml files are pulled from"""
# created on demand, so marked as optional
@@ -644,6 +642,12 @@ class PlayerFilesPath(OptionalUserFolderPath):
class Players(int):
"""amount of players, 0 to infer from player files"""
+ class AllowQuantity(Bool):
+ """
+ allow players to set an individual quantity for their yaml settings
+ with 'false' any amounts from the players will be ignored and set to 1
+ """
+
class WeightsFilePath(str):
"""
general weights file, within the stated player_files_path location
@@ -687,9 +691,9 @@ class PanicMethod(str):
start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
"""
- enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
players: Players = Players(0)
+ allow_quantity: AllowQuantity | bool = False
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
spoiler: Spoiler = Spoiler(3)
diff --git a/setup.py b/setup.py
index 3c40eab59eda..9aa240462ba9 100644
--- a/setup.py
+++ b/setup.py
@@ -201,7 +201,7 @@ def resolve_icon(icon_name: str):
icon=resolve_icon(c.icon),
))
-extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
+extra_data = ["LICENSE", "data", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
@@ -456,9 +456,8 @@ def run(self) -> None:
for world_directory in folders_to_remove)
else:
# make sure extra programs are executable
- enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
sni_exe = self.buildfolder / 'SNI/sni'
- extra_exes = (enemizer_exe, sni_exe)
+ extra_exes = (sni_exe,)
for extra_exe in extra_exes:
if extra_exe.is_file():
extra_exe.chmod(0o755)
diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py
index 0bc7b62d5b34..0906852201b4 100644
--- a/test/general/test_implemented.py
+++ b/test/general/test_implemented.py
@@ -54,7 +54,7 @@ def test_slot_data(self):
def test_no_failed_world_loads(self):
if failed_world_loads:
- self.fail(f"The following worlds failed to load: {failed_world_loads}")
+ self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}")
def test_prefill_items(self):
"""Test that every world can reach every location from allstate before pre_fill."""
diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py
index 85e239175d4d..e78d62ad406f 100644
--- a/test/general/test_rule_builder.py
+++ b/test/general/test_rule_builder.py
@@ -12,6 +12,7 @@
from rule_builder.options import Operator, OptionFilter
from rule_builder.rules import (
And,
+ AtLeast,
CanReachEntrance,
CanReachLocation,
CanReachRegion,
@@ -158,6 +159,14 @@ def get_filler_item_name(self) -> str:
@classvar_matrix(
rules=(
+ (
+ And(),
+ True_.Resolved(player=1)
+ ),
+ (
+ Or(),
+ False_.Resolved(player=1)
+ ),
(
And(Has("A", 1), Has("A", 2)),
Has.Resolved("A", 2, player=1),
@@ -250,6 +259,40 @@ def get_filler_item_name(self) -> str:
Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
),
+ (
+ AtLeast(0, Has("A")),
+ True_.Resolved(player=1),
+ ),
+ (
+ AtLeast(3, True_(), Has("A"), Has("B"), Has("C")),
+ AtLeast.Resolved(
+ (Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
+ ),
+ ),
+ (
+ AtLeast(2, False_(), Has("A"), Has("B"), Has("C")),
+ AtLeast.Resolved(
+ (Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
+ ),
+ ),
+ (
+ AtLeast(2, True_(), True_(), Has("A")),
+ True_.Resolved(player=1),
+ ),
+ (
+ AtLeast(3, Has("A"), Has("B")),
+ False_.Resolved(player=1),
+ ),
+ (
+ # This test will fail when Or(Rule, Rule) will be optimized to Rule
+ AtLeast(1, Rule(), Rule()),
+ Or.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
+ ),
+ (
+ # This test will fail when And(Rule, Rule) will be optimized to Rule
+ AtLeast(2, Rule(), Rule()),
+ And.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
+ ),
)
)
class TestSimplify(RuleBuilderTestCase):
@@ -416,6 +459,15 @@ def test_has_all_hash(self) -> None:
rule2 = HasAll("2", "2", "2", "1")
self.assertEqual(hash(rule1.resolve(world)), hash(rule2.resolve(world)))
+ def test_hash_collision(self) -> None:
+ multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
+ world = multiworld.worlds[1]
+ rule1 = Has("A", count=1).resolve(world)
+ rule2 = Has("A", count=1 << 61).resolve(world)
+ self.assertEqual(hash(rule1), hash(rule2))
+ self.assertNotEqual(rule1, rule2)
+ self.assertNotEqual(id(rule1), id(rule2))
+
class TestCaching(CachedRuleBuilderTestCase):
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
@@ -622,6 +674,24 @@ def test_has(self) -> None:
self.state.remove(item)
self.assertFalse(resolved_rule(self.state))
+ def test_at_least(self) -> None:
+ # Has has to be relied on as True_ and False_ would be optimized out
+ rule = AtLeast(2, Has("Item 1"), Has("Item 1"), Has("Item 2"), Has("Item 3"))
+ resolved_rule = rule.resolve(self.world)
+ self.world.register_rule_dependencies(resolved_rule)
+ item1 = self.world.create_item("Item 1")
+ item2 = self.world.create_item("Item 2")
+ item3 = self.world.create_item("Item 3")
+ self.assertFalse(resolved_rule(self.state))
+ self.state.collect(item1)
+ self.assertTrue(resolved_rule(self.state))
+ self.state.collect(item2)
+ self.assertTrue(resolved_rule(self.state))
+ self.state.remove(item1)
+ self.assertFalse(resolved_rule(self.state))
+ self.state.collect(item3)
+ self.assertTrue(resolved_rule(self.state))
+
def test_has_all(self) -> None:
rule = HasAll("Item 1", "Item 2")
resolved_rule = rule.resolve(self.world)
@@ -797,8 +867,13 @@ class TestSerialization(RuleBuilderTestCase):
OptionFilter(ChoiceOption, ChoiceOption.option_second, "ge"),
],
),
+ AtLeast(
+ FromWorldAttr("instance_data.at_least_requirement"),
+ Has("i15", count=2),
+ HasGroup("g2", count=3),
+ ),
CanReachEntrance("e1"),
- HasGroupUnique("g2", count=5),
+ HasGroupUnique("g3", count=5),
)
rule_dict: ClassVar[dict[str, Any]] = {
@@ -922,6 +997,29 @@ class TestSerialization(RuleBuilderTestCase):
},
],
},
+ {
+ "rule": "AtLeast",
+ "options": [],
+ "filtered_resolution": False,
+ "count": {"resolver": "FromWorldAttr", "name": "instance_data.at_least_requirement"},
+ "children": [
+ {
+ "rule": "Has",
+ "options": [],
+ "filtered_resolution": False,
+ "args": {
+ "item_name": "i15",
+ "count": 2,
+ },
+ },
+ {
+ "rule": "HasGroup",
+ "options": [],
+ "filtered_resolution": False,
+ "args": {"item_name_group": "g2", "count": 3},
+ },
+ ],
+ },
{
"rule": "CanReachEntrance",
"options": [],
@@ -932,7 +1030,7 @@ class TestSerialization(RuleBuilderTestCase):
"rule": "HasGroupUnique",
"options": [],
"filtered_resolution": False,
- "args": {"item_name_group": "g2", "count": 5},
+ "args": {"item_name_group": "g3", "count": 5},
},
],
}
@@ -964,9 +1062,15 @@ class TestExplain(RuleBuilderTestCase):
),
player=1,
),
- HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
- HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
- HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
+ AtLeast.Resolved(
+ children=(
+ HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
+ HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
+ HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
+ ),
+ count=2,
+ player=1,
+ ),
HasFromListUnique.Resolved(("Item 13", "Item 14"), player=1),
HasGroup.Resolved("Group 1", ("Item 15", "Item 16", "Item 17"), player=1),
HasGroupUnique.Resolved("Group 2", ("Item 18", "Item 19"), count=2, player=1),
@@ -1031,6 +1135,9 @@ def test_explain_json_with_state_no_items(self) -> None:
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
+ {"type": "text", "text": "At least "},
+ {"type": "color", "color": "cyan", "text": "0/2"},
+ {"type": "text", "text": " of ("},
{"type": "text", "text": "Missing "},
{"type": "color", "color": "cyan", "text": "some"},
{"type": "text", "text": " of ("},
@@ -1041,7 +1148,7 @@ def test_explain_json_with_state_no_items(self) -> None:
{"type": "color", "color": "salmon", "text": "Item 7"},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
- {"type": "text", "text": " & "},
+ {"type": "text", "text": ", "},
{"type": "text", "text": "Missing "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1052,7 +1159,7 @@ def test_explain_json_with_state_no_items(self) -> None:
{"type": "color", "color": "salmon", "text": "Item 9"},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
- {"type": "text", "text": " & "},
+ {"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "salmon", "text": "0/2"},
{"type": "text", "text": " items from ("},
@@ -1063,6 +1170,7 @@ def test_explain_json_with_state_no_items(self) -> None:
{"type": "text", "text": ", "},
{"type": "color", "color": "salmon", "text": "Item 12"},
{"type": "text", "text": ")"},
+ {"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "salmon", "text": "0/1"},
@@ -1129,6 +1237,9 @@ def test_explain_json_with_state_all_items(self) -> None:
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
+ {"type": "text", "text": "At least "},
+ {"type": "color", "color": "cyan", "text": "3/2"},
+ {"type": "text", "text": " of ("},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1139,7 +1250,7 @@ def test_explain_json_with_state_all_items(self) -> None:
{"type": "color", "color": "green", "text": "Item 7"},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
- {"type": "text", "text": " & "},
+ {"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "some"},
{"type": "text", "text": " of ("},
@@ -1150,7 +1261,7 @@ def test_explain_json_with_state_all_items(self) -> None:
{"type": "color", "color": "green", "text": "Item 9"},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
- {"type": "text", "text": " & "},
+ {"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "green", "text": "30/2"},
{"type": "text", "text": " items from ("},
@@ -1161,6 +1272,7 @@ def test_explain_json_with_state_all_items(self) -> None:
{"type": "text", "text": ", "},
{"type": "color", "color": "green", "text": "Item 12"},
{"type": "text", "text": ")"},
+ {"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "green", "text": "2/1"},
@@ -1195,7 +1307,7 @@ def test_explain_json_with_state_all_items(self) -> None:
{"type": "color", "color": "salmon", "text": "False"},
{"type": "text", "text": ")"},
]
- assert self.resolved_rule.explain_json(self.state) == expected
+ self.assertEqual(self.resolved_rule.explain_json(self.state), expected)
def test_explain_json_without_state(self) -> None:
expected: list[JSONMessagePart] = [
@@ -1223,6 +1335,9 @@ def test_explain_json_without_state(self) -> None:
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
+ {"type": "text", "text": "At least "},
+ {"type": "color", "color": "cyan", "text": "2"},
+ {"type": "text", "text": " of ("},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1232,7 +1347,7 @@ def test_explain_json_without_state(self) -> None:
{"type": "item_name", "flags": 1, "text": "Item 7", "player": 1},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
- {"type": "text", "text": " & "},
+ {"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "any"},
{"type": "text", "text": " of ("},
@@ -1242,7 +1357,7 @@ def test_explain_json_without_state(self) -> None:
{"type": "item_name", "flags": 1, "text": "Item 9", "player": 1},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
- {"type": "text", "text": " & "},
+ {"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "2"},
{"type": "text", "text": "x items from ("},
@@ -1252,6 +1367,7 @@ def test_explain_json_without_state(self) -> None:
{"type": "text", "text": ", "},
{"type": "item_name", "flags": 1, "text": "Item 12", "player": 1},
{"type": "text", "text": ")"},
+ {"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "1"},
@@ -1285,16 +1401,16 @@ def test_explain_json_without_state(self) -> None:
{"type": "color", "color": "salmon", "text": "False"},
{"type": "text", "text": ")"},
]
- assert self.resolved_rule.explain_json() == expected
+ self.assertEqual(self.resolved_rule.explain_json(), expected)
def test_explain_str_with_state_no_items(self) -> None:
expected = (
"((Missing 4x Item 1",
"| Missing some of (Missing: Item 2, Item 3)",
"| Missing all of (Missing: Item 4, Item 5))",
- "& Missing some of (Missing: Item 6 x1, Item 7 x5)",
- "& Missing all of (Missing: Item 8 x2, Item 9 x3)",
- "& Has 0/2 items from (Missing: Item 10, Item 11, Item 12)",
+ "& At least 0/2 of (Missing some of (Missing: Item 6 x1, Item 7 x5),",
+ "Missing all of (Missing: Item 8 x2, Item 9 x3),",
+ "Has 0/2 items from (Missing: Item 10, Item 11, Item 12))",
"& Has 0/1 unique items from (Missing: Item 13, Item 14)",
"& Has 0/1 items from Group 1",
"& Has 0/2 unique items from Group 2",
@@ -1304,7 +1420,7 @@ def test_explain_str_with_state_no_items(self) -> None:
"& True",
"& False)",
)
- assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
+ self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
def test_explain_str_with_state_all_items(self) -> None:
self._collect_all()
@@ -1313,9 +1429,9 @@ def test_explain_str_with_state_all_items(self) -> None:
"((Has 4x Item 1",
"| Has all of (Found: Item 2, Item 3)",
"| Has some of (Found: Item 4, Item 5))",
- "& Has all of (Found: Item 6 x1, Item 7 x5)",
- "& Has some of (Found: Item 8 x2, Item 9 x3)",
- "& Has 30/2 items from (Found: Item 10, Item 11, Item 12)",
+ "& At least 3/2 of (Has all of (Found: Item 6 x1, Item 7 x5),",
+ "Has some of (Found: Item 8 x2, Item 9 x3),",
+ "Has 30/2 items from (Found: Item 10, Item 11, Item 12))",
"& Has 2/1 unique items from (Found: Item 13, Item 14)",
"& Has 30/1 items from Group 1",
"& Has 2/2 unique items from Group 2",
@@ -1325,16 +1441,16 @@ def test_explain_str_with_state_all_items(self) -> None:
"& True",
"& False)",
)
- assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
+ self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
def test_explain_str_without_state(self) -> None:
expected = (
"((Has 4x Item 1",
"| Has all of (Item 2, Item 3)",
"| Has any of (Item 4, Item 5))",
- "& Has all of (Item 6 x1, Item 7 x5)",
- "& Has any of (Item 8 x2, Item 9 x3)",
- "& Has 2x items from (Item 10, Item 11, Item 12)",
+ "& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
+ "Has any of (Item 8 x2, Item 9 x3),",
+ "Has 2x items from (Item 10, Item 11, Item 12))",
"& Has a unique item from (Item 13, Item 14)",
"& Has an item from Group 1",
"& Has 2x unique items from Group 2",
@@ -1344,16 +1460,16 @@ def test_explain_str_without_state(self) -> None:
"& True",
"& False)",
)
- assert self.resolved_rule.explain_str() == " ".join(expected)
+ self.assertEqual(self.resolved_rule.explain_str(), " ".join(expected))
def test_str(self) -> None:
expected = (
"((Has 4x Item 1",
"| Has all of (Item 2, Item 3)",
"| Has any of (Item 4, Item 5))",
- "& Has all of (Item 6 x1, Item 7 x5)",
- "& Has any of (Item 8 x2, Item 9 x3)",
- "& Has 2x items from (Item 10, Item 11, Item 12)",
+ "& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
+ "Has any of (Item 8 x2, Item 9 x3),",
+ "Has 2x items from (Item 10, Item 11, Item 12))",
"& Has a unique item from (Item 13, Item 14)",
"& Has an item from Group 1",
"& Has 2x unique items from Group 2",
@@ -1363,7 +1479,7 @@ def test_str(self) -> None:
"& True",
"& False)",
)
- assert str(self.resolved_rule) == " ".join(expected)
+ self.assertEqual(str(self.resolved_rule), " ".join(expected))
@classvar_matrix(
diff --git a/test/netutils/test_enum.py b/test/netutils/test_enum.py
new file mode 100644
index 000000000000..3d7cdd4e332e
--- /dev/null
+++ b/test/netutils/test_enum.py
@@ -0,0 +1,37 @@
+"""Verify that NetUtils' enums work correctly with all supported Python versions."""
+
+import pickle
+import unittest
+from enum import Enum
+from typing import Type
+
+from NetUtils import ClientStatus, HintStatus, SlotType
+from Utils import restricted_loads
+
+
+class Base:
+ class DataEnumTest(unittest.TestCase):
+ type: Type[Enum]
+ value: Enum
+
+ def test_unpickle(self) -> None:
+ """Tests that enums used in multidata or multisave can be pickled and unpickled."""
+ pickled = pickle.dumps(self.value)
+ unpickled = restricted_loads(pickled)
+ self.assertEqual(unpickled, self.value)
+ self.assertIsInstance(unpickled, self.type)
+
+
+class HintStatusTest(Base.DataEnumTest):
+ type = HintStatus
+ value = HintStatus.HINT_AVOID
+
+
+class ClientStatusTest(Base.DataEnumTest):
+ type = ClientStatus
+ value = ClientStatus.CLIENT_GOAL
+
+
+class SlotTypeTest(Base.DataEnumTest):
+ type = SlotType
+ value = SlotType.player
diff --git a/test/options/test_option_classes.py b/test/options/test_option_classes.py
index ca90db88708c..8ca1751fa3e4 100644
--- a/test/options/test_option_classes.py
+++ b/test/options/test_option_classes.py
@@ -1,6 +1,8 @@
import unittest
-from Options import Choice, DefaultOnToggle, Toggle
+from collections import Counter
+
+from Options import Choice, DefaultOnToggle, Toggle, OptionDict, OptionError, OptionSet, OptionList, OptionCounter
class TestNumericOptions(unittest.TestCase):
@@ -74,3 +76,97 @@ class TestDefaultOnToggle(DefaultOnToggle):
self.assertTrue(toggle_string)
self.assertTrue(toggle_int)
self.assertTrue(toggle_alias)
+
+
+class TestContainerOptions(unittest.TestCase):
+ def test_option_dict(self):
+ class TestOptionDict(OptionDict):
+ valid_keys = frozenset({"A", "B", "C"})
+
+ unknown_key_init_dict = {"D": "Foo"}
+ test_option_dict = TestOptionDict(unknown_key_init_dict)
+ self.assertRaises(OptionError, test_option_dict.verify_keys)
+
+ init_dict = {"A": "foo", "B": "bar"}
+ test_option_dict = TestOptionDict(init_dict)
+
+ self.assertEqual(test_option_dict, init_dict) # Implicit value comparison
+ self.assertEqual(test_option_dict["A"], "foo")
+ self.assertIn("B", test_option_dict)
+ self.assertNotIn("C", test_option_dict)
+ self.assertRaises(KeyError, lambda: test_option_dict["C"])
+
+ def test_option_set(self):
+ class TestOptionSet(OptionSet):
+ valid_keys = frozenset({"A", "B", "C"})
+
+ unknown_key_init_set = {"D"}
+ test_option_set = TestOptionSet(unknown_key_init_set)
+ self.assertRaises(OptionError, test_option_set.verify_keys)
+
+ init_set = {"A", "B"}
+ test_option_set = TestOptionSet(init_set)
+
+ self.assertEqual(test_option_set, init_set) # Implicit value comparison
+ self.assertIn("B", test_option_set)
+ self.assertNotIn("C", test_option_set)
+
+ def test_option_list(self):
+ class TestOptionList(OptionList):
+ valid_keys = frozenset({"A", "B", "C"})
+
+ unknown_key_init_list = ["D"]
+ test_option_list = TestOptionList(unknown_key_init_list)
+ self.assertRaises(OptionError, test_option_list.verify_keys)
+
+ init_list = ["A", "B"]
+ test_option_list = TestOptionList(init_list)
+
+ self.assertEqual(test_option_list, init_list)
+ self.assertIn("B", test_option_list)
+ self.assertNotIn("C", test_option_list)
+
+
+ def test_option_counter(self):
+ class TestOptionCounter(OptionCounter):
+ valid_keys = frozenset({"A", "B", "C"})
+
+ max = 10
+ min = 0
+
+ unknown_key_init_dict = {"D": 5}
+ test_option_counter = TestOptionCounter(unknown_key_init_dict)
+ self.assertRaises(OptionError, test_option_counter.verify_keys)
+
+ wrong_value_type_init_dict = {"A": "B"}
+ self.assertRaises(TypeError, TestOptionCounter, wrong_value_type_init_dict)
+
+ violates_max_init_dict = {"A": 5, "B": 11}
+ test_option_counter = TestOptionCounter(violates_max_init_dict)
+ self.assertRaises(OptionError, test_option_counter.verify_values)
+
+ violates_min_init_dict = {"A": -1, "B": 5}
+ test_option_counter = TestOptionCounter(violates_min_init_dict)
+ self.assertRaises(OptionError, test_option_counter.verify_values)
+
+ init_dict = {"A": 0, "B": 10}
+ test_option_counter = TestOptionCounter(init_dict)
+ self.assertEqual(test_option_counter, Counter(init_dict))
+ self.assertIn("A", test_option_counter)
+ self.assertNotIn("C", test_option_counter)
+ self.assertEqual(test_option_counter["A"], 0)
+ self.assertEqual(test_option_counter["B"], 10)
+ self.assertEqual(test_option_counter["C"], 0)
+
+ def test_culling_option_counter(self):
+ class TestCullingCounter(OptionCounter):
+ valid_keys = frozenset({"A", "B", "C"})
+ cull_zeroes = True
+
+ init_dict = {"A": 0, "B": 10}
+ test_option_counter = TestCullingCounter(init_dict)
+ self.assertNotIn("A", test_option_counter)
+ self.assertIn("B", test_option_counter)
+ self.assertNotIn("C", test_option_counter)
+ self.assertEqual(test_option_counter["A"], 0) # It's still a Counter! cull_zeroes is about "in" checks.
+ self.assertEqual(test_option_counter, Counter({"B": 10}))
diff --git a/test/webhost/__init__.py b/test/webhost/__init__.py
index 2eb340722a3a..f6181516ceac 100644
--- a/test/webhost/__init__.py
+++ b/test/webhost/__init__.py
@@ -33,4 +33,9 @@ def setUpClass(cls) -> None:
cls.app = raw_app
def setUp(self) -> None:
+ from WebHostLib.models import db
+ from pony.orm import db_session
+ with db_session:
+ for entity in db.entities.values():
+ entity.select().delete(bulk=True)
self.client = self.app.test_client()
diff --git a/test/webhost/test_cleanup.py b/test/webhost/test_cleanup.py
new file mode 100644
index 000000000000..fc121983c906
--- /dev/null
+++ b/test/webhost/test_cleanup.py
@@ -0,0 +1,107 @@
+from datetime import timedelta
+from uuid import UUID, uuid4
+from pony.orm import db_session, commit
+
+from Utils import utcnow
+from WebHostLib.autolauncher import cleanup
+from WebHostLib.models import Room, Seed, Slot
+from . import TestBase
+
+
+class TestCleanup(TestBase):
+ def test_cleanup_unowned(self) -> None:
+ with db_session:
+ s1 = Seed(id=uuid4(), multidata=b"", owner=UUID(int=0))
+ Room(id=uuid4(), owner=UUID(int=0), seed=s1)
+
+ s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4()) # Owned
+ Room(id=uuid4(), owner=UUID(int=0), seed=s2) # Unowned room of owned seed
+
+ Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) # Unowned seed with no rooms
+
+ commit()
+
+ cleanup({"ROOM_AUTO_DELETE": 0})
+
+ with db_session:
+ self.assertEqual(Room.select().count(), 0) # Both rooms were unowned
+ self.assertEqual(Seed.select().count(), 1) # s2 is owned
+ self.assertIsNotNone(Seed.get(id=s2.id))
+
+ def test_cleanup_auto_delete(self) -> None:
+ now = utcnow()
+ old_time = now - timedelta(days=10)
+ recent_time = now - timedelta(days=2)
+
+ with db_session:
+ # Case 1: Old room, owned
+ s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
+ r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
+
+ # Case 2: Recent room, owned
+ s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
+ r2 = Room(id=uuid4(), owner=uuid4(), seed=s2, last_activity=recent_time)
+
+ # Case 3: Old seed, no rooms, owned
+ s3 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
+
+ # Case 4: Recent seed, no rooms, owned
+ s4 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=recent_time)
+
+ # Case 5: Old seed with recent room (should not be deleted)
+ s5 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
+ r5 = Room(id=uuid4(), owner=uuid4(), seed=s5, last_activity=recent_time)
+
+ commit()
+
+ # Delete items older than 5 days
+ cleanup({"ROOM_AUTO_DELETE": 5})
+
+ with db_session:
+ self.assertIsNone(Room.get(id=r1.id), "Old room should be deleted")
+ self.assertIsNotNone(Room.get(id=r2.id), "Recent room should NOT be deleted")
+ self.assertIsNone(Seed.get(id=s3.id), "Old seed without rooms should be deleted")
+ self.assertIsNotNone(Seed.get(id=s4.id), "Recent seed without rooms should NOT be deleted")
+ self.assertIsNotNone(Seed.get(id=s5.id), "Old seed with recent room should NOT be deleted")
+ self.assertIsNotNone(Room.get(id=r5.id), "Recent room for old seed should NOT be deleted")
+
+ # Seeds are deleted if they have NO rooms AND are old.
+ # After r1 is deleted, s1 has no rooms. Since it's old, it should be deleted.
+ self.assertIsNone(Seed.get(id=s1.id), "Old seed whose only room was deleted should be deleted")
+
+ def test_cleanup_disabled(self) -> None:
+ now = utcnow()
+ old_time = now - timedelta(days=10)
+
+ with db_session:
+ s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
+ r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
+ commit()
+
+ cleanup({"ROOM_AUTO_DELETE": 0})
+
+ with db_session:
+ self.assertIsNotNone(Room.get(id=r1.id), "Room should NOT be deleted when auto-delete is 0")
+ self.assertIsNotNone(Seed.get(id=s1.id), "Seed should NOT be deleted when auto-delete is 0")
+
+ def test_cleanup_slots(self) -> None:
+ now = utcnow()
+ old_time = now - timedelta(days=10)
+
+ with db_session:
+ s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
+ slot1 = Slot(player_id=1, player_name="P1", seed=s1, game="TestGame")
+
+ s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=now)
+ slot2 = Slot(player_id=2, player_name="P2", seed=s2, game="TestGame")
+
+ commit()
+
+ # Delete items older than 5 days
+ cleanup({"ROOM_AUTO_DELETE": 5})
+
+ with db_session:
+ self.assertIsNone(Seed.get(id=s1.id), "Old seed should be deleted")
+ self.assertIsNone(Slot.get(id=slot1.id), "Slot of deleted seed should be deleted")
+ self.assertIsNotNone(Seed.get(id=s2.id), "Recent seed should NOT be deleted")
+ self.assertIsNotNone(Slot.get(id=slot2.id), "Slot of recent seed should NOT be deleted")
diff --git a/test/webhost/test_port_allocation.py b/test/webhost/test_port_allocation.py
new file mode 100644
index 000000000000..914e7b2c873d
--- /dev/null
+++ b/test/webhost/test_port_allocation.py
@@ -0,0 +1,89 @@
+import os
+import unittest
+from socket import socket as Socket # noqa: N812
+
+from Utils import is_macos
+from WebHostLib.customserver import RandomPortSocketCreator
+
+ci = bool(os.environ.get("CI"))
+
+
+class TestPortAllocating(unittest.TestCase):
+ def test_parse_game_ports(self) -> None:
+ """Ensure that game ports with ranges are parsed correctly"""
+ val = RandomPortSocketCreator._parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0"))
+
+ self.assertCountEqual(val.valid_ports,
+ [*range(1000, 2001), *range(2000, 5001), *range(1000, 2001), 20, 40, 20],
+ "The parsed game ports are not the expected length")
+ self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed")
+
+ val = RandomPortSocketCreator._parse_game_ports(())
+ self.assertListEqual(val.valid_ports, [], "Empty list of game port returned something")
+ self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed")
+
+ val = RandomPortSocketCreator._parse_game_ports((0,))
+ self.assertListEqual(val.valid_ports, [], "Empty list of ranges returned something")
+ self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports")
+
+ val = RandomPortSocketCreator._parse_game_ports((1,))
+ self.assertListEqual(val.valid_ports, [1], "Valid ports doesn't contain the expected values")
+ self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed")
+
+ def test_parse_game_port_errors(self) -> None:
+ """Ensure that game ports with incorrect values raise the expected error"""
+ with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"):
+ RandomPortSocketCreator._parse_game_ports(tuple("-50215"))
+ with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"):
+ RandomPortSocketCreator._parse_game_ports(tuple("dwafawg"))
+ with self.assertRaises(
+ ValueError,
+ msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash"
+ ):
+ RandomPortSocketCreator._parse_game_ports(tuple("20-21215-"))
+ with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"):
+ RandomPortSocketCreator._parse_game_ports(tuple("f-21215"))
+
+ def test_random_port_socket_edge_cases(self) -> None:
+ """Verify if edge cases on creation of random port socket is working fine"""
+ # Try giving an empty tuple and fail over it
+ creator = RandomPortSocketCreator(())
+ with self.assertRaises(OSError) as err:
+ creator.create("127.0.0.1")
+ self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code")
+ self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string")
+
+ # Try only having ephemeral ports enabled
+ creator = RandomPortSocketCreator(("0",))
+ try:
+ creator.create("127.0.0.1").close()
+ except OSError as err:
+ self.assertEqual(err.errno, 98, "Raised an unexpected error code")
+ # If it returns our error string that means something is wrong with our code
+ self.assertNotEqual(err.strerror, "No available ports",
+ "Raised an unexpected error string")
+
+ @unittest.skipUnless(ci, "can't guarantee free ports outside of CI")
+ def test_random_port_socket(self) -> None:
+ """Verify if returned sockets use the correct port ranges"""
+ creator = RandomPortSocketCreator(("8080-8085",))
+ sockets: list[Socket] = []
+ for _ in range(6):
+ socket = creator.create("127.0.0.1")
+ sockets.append(socket)
+ _, port = socket.getsockname()
+ self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range")
+ for s in sockets:
+ s.close()
+
+ sockets.clear()
+ creator = RandomPortSocketCreator(("30000-65535",))
+ length = 5_000 if is_macos else (30_000 - len(creator._get_used_ports()))
+ for _ in range(length):
+ socket = creator.create("127.0.0.1")
+ sockets.append(socket)
+ _, port = socket.getsockname()
+ self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range")
+
+ for s in sockets:
+ s.close()
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 04f0b61ff8b9..468706a34741 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -353,6 +353,8 @@ class World(metaclass=AutoWorldRegister):
"""path it was loaded from"""
world_version: ClassVar[Version] = Version(0, 0, 0)
"""Optional world version loaded from archipelago.json"""
+ manifest: ClassVar[dict[str, Any]] = {}
+ """Mapping of the world's archipelago.json manifest. Use game and world_version attrs instead for those values."""
def __init__(self, multiworld: "MultiWorld", player: int):
assert multiworld is not None
@@ -512,7 +514,9 @@ def create_item(self, name: str) -> "Item":
def get_filler_item_name(self) -> str:
"""
- Called when the item pool needs to be filled with additional items to match location count.
+ If core AP removes an item from your item pool, this method is called to choose a replacement item
+ so item count and location count remain equal.
+ For example: plando, item_links and start_inventory_from_pool are features that may cause this.
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
For most worlds this will be one or more of your filler items, but the classification of these items
diff --git a/worlds/__init__.py b/worlds/__init__.py
index dd2d83a27ecd..8684b7424957 100644
--- a/worlds/__init__.py
+++ b/worlds/__init__.py
@@ -11,7 +11,7 @@
from pathlib import Path
from types import ModuleType
from typing import List, Sequence
-from zipfile import BadZipFile
+from zipfile import ZipFile, BadZipFile
from NetUtils import DataPackage
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
@@ -33,7 +33,7 @@
]
-failed_world_loads: List[str] = []
+failed_world_loads: dict[str, str] = {}
@dataclasses.dataclass(order=True)
@@ -68,8 +68,9 @@ def load(self) -> bool:
print(f"Could not load world {self}:", file=file_like)
traceback.print_exc(file=file_like)
file_like.seek(0)
- logging.exception(file_like.read())
- failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
+ reason = file_like.read()
+ logging.exception(reason)
+ failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason
return False
@@ -118,6 +119,7 @@ def load(self) -> bool:
game = manifest.get("game")
if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
+ AutoWorldRegister.world_types[game].manifest = manifest
if apworlds:
# encapsulation for namespace / gc purposes
@@ -128,7 +130,7 @@ def load_apworlds() -> None:
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
if add_as_failed_to_load:
- failed_world_loads.append(game_name)
+ failed_world_loads[game_name] = reason
logging.warning(reason)
for apworld_source in apworlds:
@@ -199,6 +201,15 @@ def find_spec(
# world could fail to load at this point
if apworld.world_version:
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
+
+ assert apworld.path
+ with ZipFile(apworld.path, "r") as zf:
+ manifest = apworld.read_contents(zf)
+ # version/compatible_version shouldn't be needed by world, makes it consistent with folder world
+ manifest.pop("version", None)
+ manifest.pop("compatible_version", None)
+ AutoWorldRegister.world_types[apworld.game].manifest = manifest
+
load_apworlds()
del load_apworlds
diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md
index 167c6c2faa24..214b76940f15 100644
--- a/worlds/ahit/docs/setup_en.md
+++ b/worlds/ahit/docs/setup_en.md
@@ -50,16 +50,17 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
## FAQ/Common Issues
-### The game is not connecting when starting a new save!
-For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
-(rocket icon) in-game, and re-enable the mod.
+### The game is crashing on startup repeatedly!
+This is a common issue on older versions of the game, caused by the game failing to interface with the Steam Workshop.
+To fix it you can try the following (from least to most effort required)
+- Subscribe to any random workshop mod, then unsubscribe from it
+- Restart Steam
+- Restart your computer
+- Delete the game's config directory from the files `steamapps/common/HatinTime/HatinTimeGame/Config` then verify the game files
+- Reinstall the game
### Why do relics disappear from the stands in the Spaceship after they're completed?
This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
after being completed to allow for the placement of more relics without being potentially locked out.
The level that the relic set unlocked will stay unlocked.
-
-### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
-There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
-if you have too many save files. Delete them and it should fix the problem.
diff --git a/worlds/alttp/EnemizerPatches.py b/worlds/alttp/EnemizerPatches.py
new file mode 100644
index 000000000000..9d7618c075b9
--- /dev/null
+++ b/worlds/alttp/EnemizerPatches.py
@@ -0,0 +1,478 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from functools import lru_cache
+import hashlib
+import random
+from typing import TYPE_CHECKING, Optional
+
+from Utils import pc_to_snes, snes_to_pc
+from .enemizer_data.base_patch_data import ENEMIZER_BASE_PATCHES
+from .enemizer_data.symbols import ENEMIZER_SYMBOLS
+
+if TYPE_CHECKING:
+ from . import ALTTPWorld
+ from .Rom import LocalRom
+
+
+@dataclass(frozen=True)
+class BossPatchData:
+ pointer: tuple[int, int]
+ graphics: int
+ sprite_array: tuple[int, ...]
+
+
+@dataclass(frozen=True)
+class DungeonBossPatchData:
+ room_id: int
+ sprite_pointer_address: int
+ shell_x: int
+ shell_y: int
+ clear_layer2: bool = False
+ extra_sprites: tuple[int, ...] = ()
+ gt_sprite_write_address: Optional[int] = None
+
+
+@dataclass
+class RoomObjectTable:
+ header_byte_0: int
+ header_byte_1: int
+ layer_1_objects: list[bytes] = field(default_factory=list)
+ layer_1_doors: list[bytes] = field(default_factory=list)
+ layer_2_objects: list[bytes] = field(default_factory=list)
+ layer_2_doors: list[bytes] = field(default_factory=list)
+ layer_3_objects: list[bytes] = field(default_factory=list)
+ layer_3_doors: list[bytes] = field(default_factory=list)
+
+ @classmethod
+ def from_rom(cls, rom: "LocalRom", start_address: int) -> "RoomObjectTable":
+ table = cls(rom.read_byte(start_address), rom.read_byte(start_address + 1))
+ layers = (
+ (table.layer_1_objects, table.layer_1_doors),
+ (table.layer_2_objects, table.layer_2_doors),
+ (table.layer_3_objects, table.layer_3_doors),
+ )
+ index = start_address + 2
+
+ for objects, doors in layers:
+ is_door = False
+ while True:
+ if rom.read_bytes(index, 2) == bytearray((0xF0, 0xFF)):
+ is_door = True
+ index += 2
+ continue
+ if rom.read_bytes(index, 2) == bytearray((0xFF, 0xFF)):
+ index += 2
+ break
+ if is_door:
+ doors.append(bytes(rom.read_bytes(index, 2)))
+ index += 2
+ else:
+ objects.append(bytes(rom.read_bytes(index, 3)))
+ index += 3
+
+ return table
+
+ def add_shell(self, x: int, y: int, clear_layer_2: bool, shell_id: int) -> None:
+ self.header_byte_0 = 0xF0
+ if clear_layer_2:
+ self.layer_2_objects.clear()
+ self.layer_2_objects.append(_build_subtype_3_object(x, y, shell_id))
+
+ def remove_shell(self, shell_id: int) -> None:
+ self.layer_2_objects = [obj for obj in self.layer_2_objects if _object_id(obj) != shell_id]
+
+ def to_bytes(self) -> bytes:
+ output = bytearray((self.header_byte_0, self.header_byte_1))
+ output.extend(self._serialize_layer(self.layer_1_objects, self.layer_1_doors, is_last_layer=False))
+ output.extend(self._serialize_layer(self.layer_2_objects, self.layer_2_doors, is_last_layer=False))
+ output.extend(self._serialize_layer(self.layer_3_objects, self.layer_3_doors, is_last_layer=True))
+ return bytes(output)
+
+ @staticmethod
+ def _serialize_layer(objects: list[bytes], doors: list[bytes], is_last_layer: bool) -> bytes:
+ output = bytearray()
+ for obj in objects:
+ output.extend(obj)
+ if is_last_layer or doors:
+ output.extend((0xF0, 0xFF))
+ for door in doors:
+ output.extend(door)
+ output.extend((0xFF, 0xFF))
+ return bytes(output)
+
+
+BOSS_PATCH_DATA: dict[str, BossPatchData] = {
+ "Armos": BossPatchData((0x87, 0xE8), 9, (0x05, 0x04, 0x53, 0x05, 0x07, 0x53, 0x05, 0x0A, 0x53,
+ 0x08, 0x0A, 0x53, 0x08, 0x07, 0x53, 0x08, 0x04, 0x53,
+ 0x08, 0xE7, 0x19)),
+ "Arrghus": BossPatchData((0x97, 0xD9), 20, (0x07, 0x07, 0x8C, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
+ 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
+ 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
+ 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
+ 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D)),
+ "Blind": BossPatchData((0x54, 0xE6), 32, (0x05, 0x09, 0xCE)),
+ "Helmasaur": BossPatchData((0x49, 0xE0), 21, (0x06, 0x07, 0x92)),
+ "Kholdstare": BossPatchData((0x01, 0xEA), 22, (0x05, 0x07, 0xA3, 0x05, 0x07, 0xA4, 0x05, 0x07, 0xA2)),
+ "Lanmola": BossPatchData((0xCB, 0xDC), 11, (0x07, 0x06, 0x54, 0x07, 0x09, 0x54, 0x09, 0x07, 0x54)),
+ "Moldorm": BossPatchData((0xC3, 0xD9), 12, (0x09, 0x09, 0x09)),
+ "Mothula": BossPatchData((0x31, 0xDC), 26, (0x06, 0x08, 0x88)),
+ "Trinexx": BossPatchData((0xBA, 0xE5), 23, (0x05, 0x07, 0xCB, 0x05, 0x07, 0xCC, 0x05, 0x07, 0xCD)),
+ "Vitreous": BossPatchData((0x57, 0xE4), 22, (0x05, 0x07, 0xBD)),
+}
+
+DUNGEON_BOSS_PATCH_DATA: dict[tuple[str, Optional[str]], DungeonBossPatchData] = {
+ ("Eastern Palace", None): DungeonBossPatchData(200, 0x04D7BE, 0x2B, 0x28),
+ ("Desert Palace", None): DungeonBossPatchData(51, 0x04D694, 0x0B, 0x28),
+ ("Tower of Hera", None): DungeonBossPatchData(7, 0x04D63C, 0x18, 0x16),
+ ("Palace of Darkness", None): DungeonBossPatchData(90, 0x04D6E2, 0x2B, 0x28),
+ ("Swamp Palace", None): DungeonBossPatchData(6, 0x04D63A, 0x0B, 0x28),
+ ("Skull Woods", None): DungeonBossPatchData(41, 0x04D680, 0x2B, 0x28),
+ ("Thieves Town", None): DungeonBossPatchData(172, 0x04D786, 0x2B, 0x28, clear_layer2=True),
+ ("Ice Palace", None): DungeonBossPatchData(222, 0x04D7EA, 0x2B, 0x08, clear_layer2=True),
+ ("Misery Mire", None): DungeonBossPatchData(144, 0x04D74E, 0x0B, 0x28, clear_layer2=True),
+ ("Turtle Rock", None): DungeonBossPatchData(164, 0x04D776, 0x0B, 0x28, clear_layer2=True),
+ ("Ganons Tower", "bottom"): DungeonBossPatchData(
+ 28, 0x04D666, 0x2B, 0x28, extra_sprites=(0x07, 0x07, 0xE3, 0x07, 0x08, 0xE3, 0x08, 0x07, 0xE3, 0x08, 0x08, 0xE3),
+ gt_sprite_write_address=0x04D87E,
+ ),
+ ("Ganons Tower", "middle"): DungeonBossPatchData(
+ 108, 0x04D706, 0x0B, 0x28, extra_sprites=(0x18, 0x17, 0xD1, 0x1C, 0x03, 0xC5), gt_sprite_write_address=0x04D8B6,
+ ),
+ ("Ganons Tower", "top"): DungeonBossPatchData(77, 0x04D6C8, 0x18, 0x16),
+}
+
+TRINEXX_SHELL_OBJECT_ID = 0xFF2
+KHOLDSTARE_SHELL_OBJECT_ID = 0xF95
+TRINEXX_VANILLA_ROOM_ID = 164
+KHOLDSTARE_VANILLA_ROOM_ID = 222
+ENEMY_HP_TABLE_ADDRESS = 0x6B173
+ENEMY_DAMAGE_TABLE_ADDRESS = 0x6B266
+HIDDEN_ENEMY_CHANCE_POOL_ADDRESS = 0xD7BBB
+DAMAGE_GROUP_TABLE_ADDRESS = 0x3742D
+RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS = 0x301FC
+RETRO_RUPEE_REPLACEMENT_SPRITE_ID = 0xDA
+ARROW_REFILL_5_SPRITE_ID = 0xE1
+THIEF_SPRITE_ID = 0xC4
+THIEF_DEFAULT_HP = 4
+VANILLA_HIDDEN_ENEMY_CHANCE_POOL = (
+ 0x01, 0x01, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x12,
+ 0x10, 0x01, 0x01, 0x01, 0x11, 0x01, 0x01, 0x03,
+)
+RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL = (
+ 0x01, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x12,
+ 0x0F, 0x01, 0x0F, 0x0F, 0x11, 0x0F, 0x0F, 0x03,
+)
+EXCLUDED_ENEMY_TABLE_SPRITE_IDS = frozenset({
+ 0x09, 0x53, 0x54, 0x70, 0x7A, 0x7B, 0x88, 0x89, 0x8C, 0x8D, 0x92,
+ 0xA2, 0xA3, 0xA4, 0xBD, 0xBE, 0xBF, 0xCB, 0xCC, 0xCD, 0xCE, 0xD6, 0xD7,
+})
+ENEMY_HEALTH_RANGE_BY_KEY = {
+ "easy": (1, 4),
+ "normal": (2, 15),
+ "hard": (2, 25),
+ "expert": (4, 50),
+}
+
+_ENEMIZER_SYMBOLS: Optional[dict[str, int]] = None
+
+BOSS_GFX_SHEET_INDEXES = {
+ "Agahnim1": 0x8D,
+ "Agahnim2": 0xB5,
+ "Agahnim3": 0xC8,
+ "Agahnim4": 0xB6,
+ "ArmosKnight1": 0x90,
+ "Ganon1": 0x94,
+ "Ganon2": 0xA6,
+ "Ganon3": 0xB4,
+ "Ganon4": 0xB8,
+ "Moldorm1": 0xA3,
+ "Lanmola1": 0xA4,
+ "Arrghus1": 0xAC,
+ "Mothula1": 0xAB,
+ "Helmasaure1": 0xAD,
+ "Helmasaure2": 0xB1,
+ "Blind1": 0xAE,
+ "Kholdstare1": 0xAF,
+ "Vitreous1": 0xB0,
+ "Trinexx1": 0xB2,
+ "Trinexx2": 0xB3,
+}
+
+BOSS_GFX_TABLE = {
+ "Agahnim1": (21, 190, 228),
+ "Agahnim2": (22, 255, 135),
+ "Agahnim3": (23, 220, 101),
+ "Agahnim4": (23, 132, 92),
+ "ArmosKnight1": (21, 206, 27),
+ "Ganon1": (21, 227, 160),
+ "Ganon2": (22, 186, 55),
+ "Ganon3": (22, 250, 199),
+ "Ganon4": (23, 142, 33),
+ "Moldorm1": (22, 175, 152),
+ "Lanmola1": (22, 180, 23),
+ "Arrghus1": (22, 214, 147),
+ "Mothula1": (22, 210, 84),
+ "Helmasaure1": (22, 219, 114),
+ "Helmasaure2": (22, 239, 177),
+ "Blind1": (22, 224, 90),
+ "Kholdstare1": (22, 230, 31),
+ "Vitreous1": (22, 235, 9),
+ "Trinexx1": (22, 243, 89),
+ "Trinexx2": (22, 246, 35),
+}
+
+TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS = 0x04B37E
+TRINEXX_ICE_PROJECTILE_TILE_ADDRESS = 0xE7A5
+TILE_TRAP_FLOOR_TILE_ADDRESS = 0xF3BED
+
+
+def apply_enemizer_base_patch(rom: "LocalRom") -> None:
+ for address, patch_data in _load_enemizer_base_patches():
+ rom.write_bytes(address, patch_data)
+ _apply_trinexx_room_fixes(rom)
+
+def patch_bosses(world: "ALTTPWorld", rom: "LocalRom") -> None:
+ dungeon_header_base = _get_enemizer_symbol("room_header_table")
+ moved_room_object_base = _get_enemizer_symbol("modified_room_object_table")
+ gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
+ gt_dungeon = world.dungeons[gt_dungeon_name]
+
+ placements = (
+ (world.dungeons["Eastern Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]),
+ (world.dungeons["Desert Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Desert Palace", None)]),
+ (world.dungeons["Tower of Hera"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Tower of Hera", None)]),
+ (world.dungeons["Palace of Darkness"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Palace of Darkness", None)]),
+ (world.dungeons["Swamp Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Swamp Palace", None)]),
+ (world.dungeons["Skull Woods"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Skull Woods", None)]),
+ (world.dungeons["Thieves Town"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Thieves Town", None)]),
+ (world.dungeons["Ice Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ice Palace", None)]),
+ (world.dungeons["Misery Mire"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Misery Mire", None)]),
+ (world.dungeons["Turtle Rock"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Turtle Rock", None)]),
+ (gt_dungeon.bosses["bottom"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "bottom")]),
+ (gt_dungeon.bosses["middle"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "middle")]),
+ (gt_dungeon.bosses["top"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "top")]),
+ )
+
+ modified_room_tables: dict[int, RoomObjectTable] = {}
+
+ for boss_name, dungeon_data in placements:
+ boss_data = BOSS_PATCH_DATA[boss_name]
+ rom.write_bytes(dungeon_data.sprite_pointer_address, boss_data.pointer)
+ rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 3, boss_data.graphics)
+
+ if boss_name == "Trinexx" and dungeon_data.room_id != TRINEXX_VANILLA_ROOM_ID:
+ room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
+ room_table.add_shell(
+ dungeon_data.shell_x,
+ dungeon_data.shell_y - 2,
+ dungeon_data.clear_layer2,
+ TRINEXX_SHELL_OBJECT_ID,
+ )
+ rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0x60)
+ rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x04)
+
+ if boss_name == "Kholdstare" and dungeon_data.room_id != KHOLDSTARE_VANILLA_ROOM_ID:
+ room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
+ room_table.add_shell(
+ dungeon_data.shell_x,
+ dungeon_data.shell_y,
+ dungeon_data.clear_layer2,
+ KHOLDSTARE_SHELL_OBJECT_ID,
+ )
+ rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0xE0)
+ rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x01)
+
+ if boss_name != "Trinexx" and dungeon_data.room_id == TRINEXX_VANILLA_ROOM_ID:
+ _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(TRINEXX_SHELL_OBJECT_ID)
+
+ if boss_name != "Kholdstare" and dungeon_data.room_id == KHOLDSTARE_VANILLA_ROOM_ID:
+ _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(KHOLDSTARE_SHELL_OBJECT_ID)
+
+ if dungeon_data.gt_sprite_write_address is not None:
+ _write_gt_boss_sprite_block(rom, dungeon_data, boss_data)
+
+ write_address = moved_room_object_base
+ for room_id in sorted(modified_room_tables):
+ table_bytes = modified_room_tables[room_id].to_bytes()
+ _write_room_object_pointer(rom, room_id, write_address)
+ rom.write_bytes(write_address, table_bytes)
+ write_address += len(table_bytes)
+
+ rom.write_byte(0x1B0101, 0x01)
+ rom.write_byte(0x04DE81, 0x00)
+ if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
+ rom.write_byte(0x04DE81, 0x06)
+ rom.write_byte(0x1B0101, 0x00)
+
+
+def _get_room_object_table(rom: "LocalRom", cache: dict[int, RoomObjectTable], room_id: int) -> RoomObjectTable:
+ room_table = cache.get(room_id)
+ if room_table is not None:
+ return room_table
+
+ pointer_address = 0xF8000 + (room_id * 3)
+ snes_address_bytes = rom.read_bytes(pointer_address, 3)
+ snes_address = (snes_address_bytes[2] << 16) | (snes_address_bytes[1] << 8) | snes_address_bytes[0]
+ room_table = RoomObjectTable.from_rom(rom, snes_to_pc(snes_address))
+ cache[room_id] = room_table
+ return room_table
+
+
+def _write_gt_boss_sprite_block(rom: "LocalRom", dungeon_data: DungeonBossPatchData, boss_data: BossPatchData) -> None:
+ assert dungeon_data.gt_sprite_write_address is not None
+ rom.write_int16(dungeon_data.sprite_pointer_address, dungeon_data.gt_sprite_write_address)
+
+ sprite_block = bytearray((0x00,))
+ sprite_block.extend(boss_data.sprite_array)
+ if dungeon_data.room_id == 28 and boss_data.pointer == BOSS_PATCH_DATA["Arrghus"].pointer:
+ sprite_block.extend(dungeon_data.extra_sprites[:6])
+ else:
+ sprite_block.extend(dungeon_data.extra_sprites)
+ sprite_block.append(0xFF)
+ rom.write_bytes(dungeon_data.gt_sprite_write_address, sprite_block)
+
+
+def _write_room_object_pointer(rom: "LocalRom", room_id: int, pc_address: int) -> None:
+ snes_address = pc_to_snes(pc_address)
+ pointer_address = 0xF8000 + (room_id * 3)
+ rom.write_bytes(pointer_address, (
+ snes_address & 0xFF,
+ (snes_address >> 8) & 0xFF,
+ (snes_address >> 16) & 0xFF,
+ ))
+
+
+def _build_subtype_3_object(x: int, y: int, object_id: int) -> bytes:
+ return bytes((
+ ((x << 2) & 0xFC) | (object_id & 0x03),
+ ((y << 2) & 0xFC) | ((object_id >> 2) & 0x03),
+ 0xF0 | ((object_id >> 4) & 0x0F),
+ ))
+
+
+def _object_id(object_bytes: bytes) -> Optional[int]:
+ if len(object_bytes) != 3:
+ return None
+ if object_bytes[0] >= 0xFC:
+ return (object_bytes[2] & 0x3F) + 0x100
+ if object_bytes[2] >= 0xF8:
+ return 0xF00 | ((object_bytes[2] & 0x0F) << 4) | ((object_bytes[1] & 0x03) << 2) | (object_bytes[0] & 0x03)
+ return object_bytes[2]
+
+
+def _set_enemizer_flag(rom: "LocalRom", symbol_name: str, enabled: bool) -> None:
+ rom.write_byte(_get_enemizer_symbol(symbol_name), 0x01 if enabled else 0x00)
+
+
+def _apply_killable_thief(rom: "LocalRom") -> None:
+ rom.write_byte(_get_enemizer_symbol("notItemSprite_Mimic") + 4, THIEF_SPRITE_ID)
+ thief_hp_address = ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID
+ if rom.read_byte(thief_hp_address) != 0xFF:
+ rom.write_byte(thief_hp_address, THIEF_DEFAULT_HP)
+
+
+def _randomize_enemy_health(rom: "LocalRom", rng: random.Random, enemy_health_key: str) -> None:
+ min_hp, max_hp = ENEMY_HEALTH_RANGE_BY_KEY[enemy_health_key]
+ for sprite_id in range(0xF3):
+ hp_address = ENEMY_HP_TABLE_ADDRESS + sprite_id
+ if rom.read_byte(hp_address) == 0xFF or sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
+ continue
+ rom.write_byte(hp_address, rng.randrange(min_hp, max_hp))
+
+
+def _randomize_enemy_damage(rom: "LocalRom", rng: random.Random, allow_zero_damage: bool) -> None:
+ for sprite_id in range(0xF3):
+ if sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
+ continue
+ new_damage = rng.randrange(8)
+ if not allow_zero_damage and new_damage == 2:
+ continue
+ rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + sprite_id, new_damage)
+
+
+def _shuffle_damage_groups(
+ rom: "LocalRom",
+ rng: random.Random,
+ *,
+ chaos_mode: bool,
+ allow_zero_damage: bool,
+) -> None:
+ min_damage = 0 if allow_zero_damage else 4
+ max_damage = 64 if chaos_mode else 32
+
+ for group_id in range(10):
+ green_mail_damage = rng.randrange(min_damage, max_damage)
+ if chaos_mode:
+ blue_mail_damage = rng.randrange(min_damage, max_damage)
+ red_mail_damage = rng.randrange(min_damage, max_damage)
+ else:
+ blue_mail_damage = green_mail_damage * 3 // 4
+ red_mail_damage = green_mail_damage * 3 // 8
+ group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
+ rom.write_bytes(group_address, (green_mail_damage, blue_mail_damage, red_mail_damage))
+
+
+def _update_hidden_enemy_item_table_for_retro_mode(rom: "LocalRom") -> None:
+ if rom.read_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS) != RETRO_RUPEE_REPLACEMENT_SPRITE_ID:
+ return
+
+ item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
+ for index in range(22):
+ if rom.read_byte(item_table_address + index) == ARROW_REFILL_5_SPRITE_ID:
+ rom.write_byte(item_table_address + index, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
+
+
+def _apply_trinexx_room_fixes(rom: "LocalRom") -> None:
+ # Match original Enemizer's unconditional Trinexx ice-floor removal so
+ # blue-head projectiles do not create solid walls in non-vanilla rooms.
+ rom.write_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, (0xEA, 0xEA, 0xEA, 0xEA))
+
+
+def _apply_randomized_tile_trap_floor_tile(rom: "LocalRom") -> None:
+ # Original Enemizer's RandomizeTileTrapFloorTile option changes the tile
+ # left behind by flying floor tile traps. AP does not currently expose or
+ # call this option, so keep the implementation isolated and unused.
+ rom.write_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, (0x88, 0x01))
+ rom.write_byte(TILE_TRAP_FLOOR_TILE_ADDRESS, 0x12)
+
+
+def _make_native_enemizer_rng(world: "ALTTPWorld") -> random.Random:
+ seed_material = "|".join((
+ str(world.multiworld.seed),
+ world.multiworld.seed_name,
+ str(world.player),
+ _option_key(world.options.enemy_health),
+ _option_key(world.options.enemy_damage),
+ str(int(bool(world.options.enemy_shuffle))),
+ str(int(bool(world.options.bush_shuffle))),
+ str(int(bool(world.options.killable_thieves))),
+ ))
+ seed = int.from_bytes(hashlib.sha256(seed_material.encode("utf-8")).digest()[:8], "big")
+ return random.Random(seed)
+
+
+@lru_cache(maxsize=1)
+def _load_enemizer_base_patches() -> tuple[tuple[int, bytes], ...]:
+ return tuple(
+ (entry.address, entry.patch_data)
+ for entry in ENEMIZER_BASE_PATCHES
+ )
+
+
+def _option_key(option: object) -> str:
+ return str(getattr(option, "current_key", option))
+
+
+def _get_enemizer_symbol(symbol_name: str) -> int:
+ global _ENEMIZER_SYMBOLS
+ if _ENEMIZER_SYMBOLS is None:
+ _ENEMIZER_SYMBOLS = _load_enemizer_symbols()
+ return _ENEMIZER_SYMBOLS[symbol_name]
+
+
+def _load_enemizer_symbols() -> dict[str, int]:
+ return {
+ name: snes_to_pc(snes_address)
+ for name, snes_address in ENEMIZER_SYMBOLS.items()
+ }
diff --git a/worlds/alttp/EnemyShuffle.py b/worlds/alttp/EnemyShuffle.py
new file mode 100644
index 000000000000..3f09920169d4
--- /dev/null
+++ b/worlds/alttp/EnemyShuffle.py
@@ -0,0 +1,1707 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional, TYPE_CHECKING
+
+from Utils import snes_to_pc
+
+from .EnemizerPatches import apply_enemizer_base_patch
+from .Rom import LocalRom, get_base_rom_path
+from .enemizer_data.dungeon_sprite_addresses import DUNGEON_SPRITE_ADDRESSES, KEYED_SPRITE_ID_ADDRESSES
+from .enemizer_data.enemy_room_metadata import (
+ BOSS_ROOM_IDS,
+ DONT_RANDOMIZE_ROOM_IDS,
+ NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS,
+ ROOM_GROUP_REQUIREMENTS,
+ SHUTTER_ROOM_IDS,
+ WATER_ROOM_IDS,
+)
+from .enemizer_data.enemy_sprite_requirements import ENEMY_SPRITE_REQUIREMENTS
+from .enemizer_data.overworld_enemy_metadata import (
+ AREA_IDS,
+ DO_NOT_RANDOMIZE_AREA_IDS,
+ FORCED_GROUP_REQUIREMENTS,
+)
+from .enemizer_data.symbols import ENEMIZER_SYMBOLS
+
+if TYPE_CHECKING:
+ from . import ALTTPWorld
+ from .Rom import LocalRom
+
+
+DUNGEON_HEADER_POINTER_TABLE_BASE = 0x271E2
+DUNGEON_SPRITE_POINTER_TABLE_BASE = 0x4D62E
+OVERWORLD_SPRITE_POINTER_TABLE_BASE = 0x4C901
+OVERWORLD_AREA_GRAPHICS_BLOCK_BASE = 0x7A81
+ROOM_HEADER_BANK_LOCATION = 0xB5E7
+SPRITE_GROUP_BASE_ADDRESS = 0x5B97
+TOTAL_SPRITE_GROUPS = 144
+TOTAL_DUNGEON_ROOMS = 0x128
+
+SPRITE_OVERLORD_MASK = 0xE0
+SPRITE_OVERLORD_REMOVE_MASK = 0x1F
+SPRITE_SUBTYPE_BYTE_0_MASK = 0x60
+KEY_SPRITE_ID = 0xE4
+BIG_KEY_SPRITE_ID = 0xE5
+WALLMASTER_SPRITE_ID = 0x90
+STAL_SPRITE_ID = 0xD3
+FLOPPING_FISH_SPRITE_ID = 0xD2
+OW_FALLING_ROCKS_SPRITE_ID = 0xF4
+OW_WALLMASTER_TO_HOULIHAN_SPRITE_ID = 0xFB
+WATER_TEKTITE_SPRITE_ID = 0x81
+POTENTIAL_SUBGROUP_0 = (22, 31, 47, 14)
+POTENTIAL_SUBGROUP_1 = (44, 30, 32)
+POTENTIAL_SUBGROUP_2 = (12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42)
+POTENTIAL_SUBGROUP_3 = (17, 16, 27, 20, 82, 83)
+GUARD_SUBGROUP_1_DUNGEON_GROUP_IDS = frozenset((1, 2, 3, 4))
+SELECTED_BOSS_GROUP_REQUIREMENTS = {
+ "Armos": (9, 83),
+ "Lanmola": (11, 84),
+ "Moldorm": (12, 9),
+ "Arrghus": (20, 140),
+ "Helmasaur": (21, 146),
+ "Kholdstare": (22, 162),
+ "Vitreous": (22, 189),
+ "Trinexx": (23, 203),
+ "Mothula": (26, 136),
+ "Blind": (32, 206),
+}
+
+@dataclass(frozen=True)
+class RoomGroupRequirement:
+ group_id: Optional[int]
+ subgroup_0: Optional[int]
+ subgroup_1: Optional[int]
+ subgroup_2: Optional[int]
+ subgroup_3: Optional[int]
+ rooms: tuple[int, ...]
+
+
+@dataclass(frozen=True)
+class OverworldGroupRequirement:
+ group_id: Optional[int]
+ subgroup_0: Optional[int]
+ subgroup_1: Optional[int]
+ subgroup_2: Optional[int]
+ subgroup_3: Optional[int]
+ areas: tuple[int, ...]
+
+
+@dataclass
+class DungeonSpriteGroup:
+ group_id: int
+ dungeon_group_id: int
+ subgroup_0: int
+ subgroup_1: int
+ subgroup_2: int
+ subgroup_3: int
+ preserve_subgroup_0: bool = False
+ preserve_subgroup_1: bool = False
+ preserve_subgroup_2: bool = False
+ preserve_subgroup_3: bool = False
+
+
+@dataclass(frozen=True)
+class EnemySpriteRequirement:
+ sprite_name: str
+ sprite_id: int
+ boss: bool
+ overlord: bool
+ do_not_randomize: bool
+ killable: bool
+ npc: bool
+ never_use_dungeon: bool
+ never_use_overworld: bool
+ cannot_have_key: bool
+ is_object: bool
+ absorbable: bool
+ is_water_sprite: bool
+ is_enemy_sprite: bool
+ group_ids: tuple[int, ...]
+ subgroup_0: tuple[int, ...]
+ subgroup_1: tuple[int, ...]
+ subgroup_2: tuple[int, ...]
+ subgroup_3: tuple[int, ...]
+ parameters: Optional[int]
+ special_glitched: bool
+ excluded_rooms: tuple[int, ...]
+ dont_randomize_rooms: tuple[int, ...]
+ spawnable_rooms: tuple[int, ...]
+
+
+@dataclass(frozen=True)
+class DungeonEnemySprite:
+ address: int
+ byte_0: int
+ byte_1: int
+ sprite_id: int
+ is_overlord: bool
+ has_key: bool
+
+ @property
+ def is_on_bg2(self) -> bool:
+ return bool(self.byte_0 & 0x80)
+
+ @property
+ def hm_param(self) -> int:
+ return ((self.byte_0 & 0x60) >> 2) | ((self.byte_1 & 0xE0) >> 5)
+
+ @property
+ def y_coord_pixels(self) -> int:
+ return (self.byte_0 & 0x1F) * 16
+
+ @property
+ def x_coord_pixels(self) -> int:
+ return (self.byte_1 & 0x1F) * 16
+
+
+@dataclass(frozen=True)
+class DungeonEnemyRoom:
+ room_id: int
+ room_header_address: int
+ sprite_table_address: int
+ graphics_block_id: int
+ tag_1: int
+ tag_2: int
+ sort_sprites_value: int
+ sprites: tuple[DungeonEnemySprite, ...]
+ required_group_id: Optional[int]
+ required_subgroup_0: tuple[int, ...]
+ required_subgroup_1: tuple[int, ...]
+ required_subgroup_2: tuple[int, ...]
+ required_subgroup_3: tuple[int, ...]
+ is_shutter_room: bool
+ is_water_room: bool
+ do_not_randomize: bool
+ no_special_enemies_standard: bool
+
+
+@dataclass(frozen=True)
+class RandomizedDungeonEnemySprite:
+ address: int
+ byte_0: int
+ byte_1: int
+ original_sprite_id: int
+ sprite_id: int
+ is_overlord: bool
+ has_key: bool
+
+
+@dataclass(frozen=True)
+class RandomizedDungeonEnemyRoom:
+ room_id: int
+ room_header_address: int
+ sprite_table_address: int
+ original_graphics_block_id: int
+ graphics_block_id: int
+ tag_1: int
+ tag_2: int
+ sort_sprites_value: int
+ sprites: tuple[RandomizedDungeonEnemySprite, ...]
+ skipped_randomization: bool
+
+
+@dataclass(frozen=True)
+class OverworldEnemySprite:
+ address: int
+ y_coord: int
+ x_coord: int
+ sprite_id: int
+
+
+@dataclass(frozen=True)
+class OverworldEnemyArea:
+ area_id: int
+ sprite_table_address: int
+ graphics_block_address: int
+ graphics_block_id: int
+ bush_sprite_id: int
+ sprites: tuple[OverworldEnemySprite, ...]
+ do_not_randomize: bool
+
+
+@dataclass(frozen=True)
+class RandomizedOverworldEnemySprite:
+ address: int
+ y_coord: int
+ x_coord: int
+ original_sprite_id: int
+ sprite_id: int
+
+
+@dataclass(frozen=True)
+class RandomizedOverworldEnemyArea:
+ area_id: int
+ sprite_table_address: int
+ graphics_block_address: int
+ original_graphics_block_id: int
+ graphics_block_id: int
+ original_bush_sprite_id: int
+ bush_sprite_id: int
+ sprites: tuple[RandomizedOverworldEnemySprite, ...]
+ skipped_randomization: bool
+
+
+@dataclass(frozen=True)
+class EnemyShuffleState:
+ dungeon_rooms: dict[int, DungeonEnemyRoom]
+ overworld_areas: dict[int, OverworldEnemyArea]
+ sprite_groups: dict[int, DungeonSpriteGroup]
+ sprite_requirements: tuple[EnemySpriteRequirement, ...]
+ room_group_requirements: tuple[RoomGroupRequirement, ...]
+ overworld_group_requirements: tuple[OverworldGroupRequirement, ...]
+ shutter_room_ids: frozenset[int]
+ water_room_ids: frozenset[int]
+ dont_randomize_room_ids: frozenset[int]
+ no_special_enemies_standard_room_ids: frozenset[int]
+ boss_room_ids: frozenset[int]
+ dont_randomize_overworld_area_ids: frozenset[int]
+ randomized_dungeon_rooms: dict[int, RandomizedDungeonEnemyRoom]
+ randomized_overworld_areas: dict[int, RandomizedOverworldEnemyArea]
+
+
+def generate_enemy_shuffle_state(world: "ALTTPWorld") -> EnemyShuffleState:
+ rom_bytes = _get_base_patched_rom_bytes()
+ moved_header_bank = _get_enemizer_symbol("moved_room_header_bank_value_address")
+ bush_spawn_table_address = _get_enemizer_symbol("sprite_bush_spawn_table_overworld")
+ metadata = _load_enemy_room_metadata()
+ overworld_metadata = _load_overworld_enemy_metadata()
+ sprite_requirements = _load_enemy_sprite_requirements()
+ dungeon_rooms = {
+ room.room_id: room
+ for room in _read_dungeon_rooms(rom_bytes, moved_header_bank, metadata)
+ }
+ overworld_areas = {
+ area.area_id: area
+ for area in _read_overworld_areas(rom_bytes, bush_spawn_table_address, overworld_metadata)
+ }
+ sprite_groups = {
+ group.group_id: group
+ for group in _read_sprite_groups(rom_bytes)
+ }
+ _setup_required_dungeon_groups(world, sprite_groups, metadata["room_requirements"])
+ _apply_selected_boss_group_requirements(world, sprite_groups, sprite_requirements)
+ _randomize_dungeon_groups(world, sprite_groups)
+ randomized_dungeon_rooms = _randomize_dungeon_rooms(
+ world,
+ dungeon_rooms,
+ sprite_groups,
+ sprite_requirements,
+ )
+ _setup_required_overworld_groups(sprite_groups, overworld_metadata["forced_group_requirements"])
+ _randomize_overworld_groups(world, sprite_groups)
+ randomized_overworld_areas = _randomize_overworld_areas(
+ world,
+ overworld_areas,
+ sprite_groups,
+ sprite_requirements,
+ overworld_metadata["forced_group_requirements"],
+ )
+ state = EnemyShuffleState(
+ dungeon_rooms=dungeon_rooms,
+ overworld_areas=overworld_areas,
+ sprite_groups=sprite_groups,
+ sprite_requirements=sprite_requirements,
+ room_group_requirements=metadata["room_requirements"],
+ overworld_group_requirements=overworld_metadata["forced_group_requirements"],
+ shutter_room_ids=metadata["shutter_room_ids"],
+ water_room_ids=metadata["water_room_ids"],
+ dont_randomize_room_ids=metadata["dont_randomize_room_ids"],
+ no_special_enemies_standard_room_ids=metadata["no_special_enemies_standard_room_ids"],
+ boss_room_ids=metadata["boss_room_ids"],
+ dont_randomize_overworld_area_ids=overworld_metadata["do_not_randomize_area_ids"],
+ randomized_dungeon_rooms=randomized_dungeon_rooms,
+ randomized_overworld_areas=randomized_overworld_areas,
+ )
+ validate_enemy_shuffle_state(state, is_standard_mode=world.options.mode == "standard")
+ return state
+
+
+def _get_base_patched_rom_bytes() -> bytes:
+ patched_rom_bytes = getattr(_get_base_patched_rom_bytes, "patched_rom_bytes", None)
+ if patched_rom_bytes is None:
+ patched_rom = LocalRom(get_base_rom_path())
+ apply_enemizer_base_patch(patched_rom)
+ patched_rom_bytes = bytes(patched_rom.buffer)
+ _get_base_patched_rom_bytes.patched_rom_bytes = patched_rom_bytes
+ return patched_rom_bytes
+
+
+def _read_dungeon_rooms(rom_bytes: bytes, moved_header_bank_address: int, metadata: dict[str, object]) -> list[DungeonEnemyRoom]:
+ rooms: list[DungeonEnemyRoom] = []
+ room_header_bank = _get_room_header_bank(rom_bytes, moved_header_bank_address)
+ dungeon_sprite_metadata = _load_dungeon_sprite_metadata()
+ shutter_room_ids = metadata["shutter_room_ids"]
+ water_room_ids = metadata["water_room_ids"]
+ dont_randomize_room_ids = metadata["dont_randomize_room_ids"]
+ no_special_enemies_standard_room_ids = metadata["no_special_enemies_standard_room_ids"]
+ room_requirements = metadata["room_requirements"]
+
+ for room_id in range(TOTAL_DUNGEON_ROOMS):
+ room_header_address = _read_room_header_address(rom_bytes, room_id, room_header_bank)
+ sprite_table_address = _read_room_sprite_table_address(rom_bytes, room_id)
+ merged_requirement = _merge_room_requirements(room_id, room_requirements)
+ rooms.append(
+ DungeonEnemyRoom(
+ room_id=room_id,
+ room_header_address=room_header_address,
+ sprite_table_address=sprite_table_address,
+ graphics_block_id=rom_bytes[room_header_address + 3],
+ tag_1=rom_bytes[room_header_address + 5],
+ tag_2=rom_bytes[room_header_address + 6],
+ sort_sprites_value=rom_bytes[sprite_table_address],
+ sprites=_read_room_sprites(rom_bytes, room_id, sprite_table_address, dungeon_sprite_metadata),
+ required_group_id=merged_requirement.group_id,
+ required_subgroup_0=merged_requirement.subgroup_0,
+ required_subgroup_1=merged_requirement.subgroup_1,
+ required_subgroup_2=merged_requirement.subgroup_2,
+ required_subgroup_3=merged_requirement.subgroup_3,
+ is_shutter_room=room_id in shutter_room_ids,
+ is_water_room=room_id in water_room_ids,
+ do_not_randomize=room_id in dont_randomize_room_ids,
+ no_special_enemies_standard=room_id in no_special_enemies_standard_room_ids,
+ )
+ )
+
+ return rooms
+
+
+def _get_room_header_bank(rom_bytes: bytes, moved_header_bank_address: int) -> int:
+ if 0 <= moved_header_bank_address < len(rom_bytes):
+ moved_header_bank = rom_bytes[moved_header_bank_address]
+ if moved_header_bank:
+ return moved_header_bank
+ return rom_bytes[ROOM_HEADER_BANK_LOCATION]
+
+
+def _read_sprite_groups(rom_bytes: bytes) -> tuple[DungeonSpriteGroup, ...]:
+ groups = []
+ for group_id in range(TOTAL_SPRITE_GROUPS):
+ groups.append(
+ DungeonSpriteGroup(
+ group_id=group_id,
+ dungeon_group_id=group_id - 0x40,
+ subgroup_0=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4)],
+ subgroup_1=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 1],
+ subgroup_2=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 2],
+ subgroup_3=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 3],
+ )
+ )
+ return tuple(groups)
+
+
+def _setup_required_dungeon_groups(
+ world: "ALTTPWorld",
+ sprite_groups: dict[int, DungeonSpriteGroup],
+ room_requirements: tuple[RoomGroupRequirement, ...],
+) -> None:
+ for requirement in room_requirements:
+ if requirement.group_id is None:
+ continue
+ group = sprite_groups.get(requirement.group_id + 0x40)
+ if group is None:
+ continue
+ _apply_required_subgroups(group, requirement)
+
+ merged_room_requirements = {
+ room_id: _merge_room_requirements(room_id, room_requirements)
+ for requirement in room_requirements
+ for room_id in requirement.rooms
+ }
+
+ for merged_requirement in merged_room_requirements.values():
+ if merged_requirement.group_id is not None:
+ continue
+ if _has_preserved_group_for_room_requirement(sprite_groups, merged_requirement):
+ continue
+
+ possible_groups = [
+ group for group in sprite_groups.values()
+ if 0 < group.dungeon_group_id < 60
+ and (
+ not group.preserve_subgroup_0
+ or not group.preserve_subgroup_1
+ or not group.preserve_subgroup_2
+ or not group.preserve_subgroup_3
+ )
+ and (not merged_requirement.subgroup_0 or not group.preserve_subgroup_0)
+ and (not merged_requirement.subgroup_1 or not group.preserve_subgroup_1)
+ and (not merged_requirement.subgroup_2 or not group.preserve_subgroup_2)
+ and (not merged_requirement.subgroup_3 or not group.preserve_subgroup_3)
+ ]
+ if not possible_groups:
+ continue
+
+ selected_group = world.random.choice(possible_groups)
+ _apply_merged_room_requirement(selected_group, merged_requirement)
+
+
+def _apply_selected_boss_group_requirements(
+ world: "ALTTPWorld",
+ sprite_groups: dict[int, DungeonSpriteGroup],
+ sprite_requirements: tuple[EnemySpriteRequirement, ...],
+) -> None:
+ requirement_by_sprite_id = {requirement.sprite_id: requirement for requirement in sprite_requirements}
+ for boss_name in _get_selected_boss_names(world):
+ boss_group_data = SELECTED_BOSS_GROUP_REQUIREMENTS.get(boss_name)
+ if boss_group_data is None:
+ continue
+ dungeon_group_id, sprite_id = boss_group_data
+ group = sprite_groups.get(dungeon_group_id + 0x40)
+ requirement = requirement_by_sprite_id.get(sprite_id)
+ if group is None or requirement is None:
+ continue
+ _apply_selected_boss_requirement(group, requirement)
+
+
+def _get_selected_boss_names(world: "ALTTPWorld") -> tuple[str, ...]:
+ dungeons = getattr(world, "dungeons", None)
+ if not dungeons:
+ return tuple()
+
+ gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
+ gt_dungeon = dungeons.get(gt_dungeon_name)
+ gt_bosses = getattr(gt_dungeon, "bosses", {}) if gt_dungeon is not None else {}
+
+ selected_bosses = [
+ dungeons["Eastern Palace"].boss.enemizer_name,
+ dungeons["Desert Palace"].boss.enemizer_name,
+ dungeons["Tower of Hera"].boss.enemizer_name,
+ dungeons["Palace of Darkness"].boss.enemizer_name,
+ dungeons["Swamp Palace"].boss.enemizer_name,
+ dungeons["Skull Woods"].boss.enemizer_name,
+ dungeons["Thieves Town"].boss.enemizer_name,
+ dungeons["Ice Palace"].boss.enemizer_name,
+ dungeons["Misery Mire"].boss.enemizer_name,
+ dungeons["Turtle Rock"].boss.enemizer_name,
+ ]
+ for gt_slot in ("bottom", "middle", "top"):
+ if gt_slot in gt_bosses:
+ selected_bosses.append(gt_bosses[gt_slot].enemizer_name)
+ return tuple(selected_bosses)
+
+
+def _apply_selected_boss_requirement(group: DungeonSpriteGroup, requirement: EnemySpriteRequirement) -> None:
+ if requirement.subgroup_0:
+ group.subgroup_0 = requirement.subgroup_0[0]
+ group.preserve_subgroup_0 = True
+ if requirement.subgroup_1:
+ group.subgroup_1 = requirement.subgroup_1[0]
+ group.preserve_subgroup_1 = True
+ if requirement.subgroup_2:
+ group.subgroup_2 = requirement.subgroup_2[0]
+ group.preserve_subgroup_2 = True
+ if requirement.subgroup_3:
+ group.subgroup_3 = requirement.subgroup_3[0]
+ group.preserve_subgroup_3 = True
+
+
+def _setup_required_overworld_groups(
+ sprite_groups: dict[int, DungeonSpriteGroup],
+ overworld_group_requirements: tuple[OverworldGroupRequirement, ...],
+) -> None:
+ for requirement in overworld_group_requirements:
+ if requirement.group_id is None:
+ continue
+ group = sprite_groups.get(requirement.group_id)
+ if group is None:
+ continue
+ if (
+ requirement.subgroup_0 is None
+ and requirement.subgroup_1 is None
+ and requirement.subgroup_2 is None
+ and requirement.subgroup_3 is None
+ ):
+ group.preserve_subgroup_0 = True
+ group.preserve_subgroup_1 = True
+ group.preserve_subgroup_2 = True
+ group.preserve_subgroup_3 = True
+ continue
+ _apply_required_subgroups(group, requirement)
+
+
+def _apply_required_subgroups(group: DungeonSpriteGroup, requirement: RoomGroupRequirement | OverworldGroupRequirement) -> None:
+ if requirement.subgroup_0 is not None:
+ group.subgroup_0 = requirement.subgroup_0
+ group.preserve_subgroup_0 = True
+ if requirement.subgroup_1 is not None:
+ group.subgroup_1 = requirement.subgroup_1
+ group.preserve_subgroup_1 = True
+ if requirement.subgroup_2 is not None:
+ group.subgroup_2 = requirement.subgroup_2
+ group.preserve_subgroup_2 = True
+ if requirement.subgroup_3 is not None:
+ group.subgroup_3 = requirement.subgroup_3
+ group.preserve_subgroup_3 = True
+
+
+def _apply_merged_room_requirement(group: DungeonSpriteGroup, requirement: MergedRoomRequirement) -> None:
+ if requirement.subgroup_0:
+ group.subgroup_0 = requirement.subgroup_0[0]
+ group.preserve_subgroup_0 = True
+ if requirement.subgroup_1:
+ group.subgroup_1 = requirement.subgroup_1[0]
+ group.preserve_subgroup_1 = True
+ if requirement.subgroup_2:
+ group.subgroup_2 = requirement.subgroup_2[0]
+ group.preserve_subgroup_2 = True
+ if requirement.subgroup_3:
+ group.subgroup_3 = requirement.subgroup_3[0]
+ group.preserve_subgroup_3 = True
+
+
+def _has_preserved_group_for_room_requirement(
+ sprite_groups: dict[int, DungeonSpriteGroup],
+ requirement: MergedRoomRequirement,
+) -> bool:
+ for group in sprite_groups.values():
+ if not (0 < group.dungeon_group_id < 60):
+ continue
+ if requirement.subgroup_0 and (group.subgroup_0 != requirement.subgroup_0[0] or not group.preserve_subgroup_0):
+ continue
+ if requirement.subgroup_1 and (group.subgroup_1 != requirement.subgroup_1[0] or not group.preserve_subgroup_1):
+ continue
+ if requirement.subgroup_2 and (group.subgroup_2 != requirement.subgroup_2[0] or not group.preserve_subgroup_2):
+ continue
+ if requirement.subgroup_3 and (group.subgroup_3 != requirement.subgroup_3[0] or not group.preserve_subgroup_3):
+ continue
+ return True
+ return False
+
+
+def _randomize_dungeon_groups(world: "ALTTPWorld", sprite_groups: dict[int, DungeonSpriteGroup]) -> None:
+ for group in sprite_groups.values():
+ if not (0 < group.dungeon_group_id < 60):
+ continue
+ if not group.preserve_subgroup_1 and group.dungeon_group_id in GUARD_SUBGROUP_1_DUNGEON_GROUP_IDS:
+ group.preserve_subgroup_1 = True
+ group.subgroup_1 = world.random.choice((73, 13))
+ if not group.preserve_subgroup_0:
+ group.subgroup_0 = world.random.choice(POTENTIAL_SUBGROUP_0)
+ if not group.preserve_subgroup_1:
+ group.subgroup_1 = world.random.choice(POTENTIAL_SUBGROUP_1)
+ if not group.preserve_subgroup_2:
+ group.subgroup_2 = world.random.choice(POTENTIAL_SUBGROUP_2)
+ if not group.preserve_subgroup_3:
+ group.subgroup_3 = world.random.choice(POTENTIAL_SUBGROUP_3)
+
+
+def _randomize_overworld_groups(world: "ALTTPWorld", sprite_groups: dict[int, DungeonSpriteGroup]) -> None:
+ for group in sprite_groups.values():
+ if not (0 < group.group_id < 0x40):
+ continue
+ if not group.preserve_subgroup_0:
+ group.subgroup_0 = world.random.choice(POTENTIAL_SUBGROUP_0)
+ if not group.preserve_subgroup_1:
+ group.subgroup_1 = world.random.choice(POTENTIAL_SUBGROUP_1)
+ if not group.preserve_subgroup_2:
+ group.subgroup_2 = world.random.choice(POTENTIAL_SUBGROUP_2)
+ if not group.preserve_subgroup_3:
+ group.subgroup_3 = world.random.choice(POTENTIAL_SUBGROUP_3)
+
+
+def _read_room_header_address(rom_bytes: bytes, room_id: int, room_header_bank: int) -> int:
+ pointer_address = DUNGEON_HEADER_POINTER_TABLE_BASE + (room_id * 2)
+ snes_address = (
+ rom_bytes[pointer_address]
+ | (rom_bytes[pointer_address + 1] << 8)
+ | (room_header_bank << 16)
+ )
+ return snes_to_pc(snes_address)
+
+
+def _read_room_sprite_table_address(rom_bytes: bytes, room_id: int) -> int:
+ pointer_address = DUNGEON_SPRITE_POINTER_TABLE_BASE + (room_id * 2)
+ snes_address = (
+ rom_bytes[pointer_address]
+ | (rom_bytes[pointer_address + 1] << 8)
+ | (0x09 << 16)
+ )
+ return snes_to_pc(snes_address)
+
+
+def _read_overworld_areas(
+ rom_bytes: bytes,
+ bush_spawn_table_address: int,
+ metadata: dict[str, object],
+) -> list[OverworldEnemyArea]:
+ areas: list[OverworldEnemyArea] = []
+ do_not_randomize_area_ids = metadata["do_not_randomize_area_ids"]
+
+ for area_id in metadata["area_ids"]:
+ sprite_table_address = _read_overworld_sprite_table_address(rom_bytes, area_id)
+ graphics_block_address = _get_overworld_graphics_block_address(area_id)
+ areas.append(
+ OverworldEnemyArea(
+ area_id=area_id,
+ sprite_table_address=sprite_table_address,
+ graphics_block_address=graphics_block_address,
+ graphics_block_id=rom_bytes[graphics_block_address],
+ bush_sprite_id=rom_bytes[bush_spawn_table_address + area_id],
+ sprites=_read_overworld_sprites(rom_bytes, sprite_table_address),
+ do_not_randomize=area_id in do_not_randomize_area_ids,
+ )
+ )
+
+ return areas
+
+
+def _read_overworld_sprite_table_address(rom_bytes: bytes, area_id: int) -> int:
+ pointer_address = OVERWORLD_SPRITE_POINTER_TABLE_BASE + (area_id * 2)
+ snes_address = (
+ rom_bytes[pointer_address]
+ | (rom_bytes[pointer_address + 1] << 8)
+ | (0x09 << 16)
+ )
+ return snes_to_pc(snes_address)
+
+
+def _get_overworld_graphics_block_address(area_id: int) -> int:
+ if area_id in {0x80, 0x81}:
+ return 0x16576 + (area_id - 0x80)
+ if area_id in {0x110, 0x111}:
+ return 0x16576 + (area_id - 0x110)
+
+ address = OVERWORLD_AREA_GRAPHICS_BLOCK_BASE + area_id
+ if 0x40 <= area_id < 0x80:
+ address += 0x40
+ if 0x90 <= area_id < 0x110:
+ address -= 0x50
+ return address
+
+
+def _read_overworld_sprites(rom_bytes: bytes, sprite_table_address: int) -> tuple[OverworldEnemySprite, ...]:
+ sprites: list[OverworldEnemySprite] = []
+ index = sprite_table_address
+ while rom_bytes[index] != 0xFF:
+ sprites.append(
+ OverworldEnemySprite(
+ address=index,
+ y_coord=rom_bytes[index],
+ x_coord=rom_bytes[index + 1],
+ sprite_id=rom_bytes[index + 2],
+ )
+ )
+ index += 3
+ return tuple(sprites)
+
+
+def _read_room_sprites(
+ rom_bytes: bytes,
+ room_id: int,
+ sprite_table_address: int,
+ dungeon_sprite_metadata: dict[str, object],
+) -> tuple[DungeonEnemySprite, ...]:
+ sprites: list[DungeonEnemySprite] = []
+ keyed_sprite_id_addresses = dungeon_sprite_metadata["keyed_sprite_id_addresses"]
+ editable_sprite_id_addresses = dungeon_sprite_metadata["room_sprite_id_addresses"].get(room_id)
+
+ if editable_sprite_id_addresses is None:
+ sprite_addresses = []
+ index = sprite_table_address + 1 # byte 0 is sort-sprites metadata
+ while rom_bytes[index] != 0xFF:
+ sprite_addresses.append(index)
+ index += 3
+ else:
+ sprite_addresses = [sprite_id_address - 2 for sprite_id_address in editable_sprite_id_addresses]
+
+ seen_sprite_addresses: set[int] = set()
+ unique_sprite_addresses = []
+ for address in sprite_addresses:
+ if address in seen_sprite_addresses:
+ continue
+ seen_sprite_addresses.add(address)
+ unique_sprite_addresses.append(address)
+
+ for index in unique_sprite_addresses:
+ byte_0 = rom_bytes[index]
+ byte_1 = rom_bytes[index + 1]
+ sprite_id = rom_bytes[index + 2]
+ is_overlord = (byte_1 & SPRITE_OVERLORD_MASK) == SPRITE_OVERLORD_MASK and (
+ (byte_0 & SPRITE_SUBTYPE_BYTE_0_MASK) != SPRITE_SUBTYPE_BYTE_0_MASK
+ )
+ if not is_overlord and sprite_id not in {KEY_SPRITE_ID, WALLMASTER_SPRITE_ID}:
+ byte_0 &= 0x9F
+ byte_1 &= SPRITE_OVERLORD_REMOVE_MASK
+ has_key = (index + 2) in keyed_sprite_id_addresses
+ sprites.append(
+ DungeonEnemySprite(
+ address=index,
+ byte_0=byte_0,
+ byte_1=byte_1,
+ sprite_id=sprite_id + (0x100 if is_overlord else 0),
+ is_overlord=is_overlord,
+ has_key=has_key,
+ )
+ )
+
+ return tuple(sprites)
+
+
+def _get_enemizer_symbol(symbol_name: str) -> int:
+ return snes_to_pc(ENEMIZER_SYMBOLS[symbol_name])
+
+
+def _load_enemy_room_metadata() -> dict[str, object]:
+ return {
+ "shutter_room_ids": SHUTTER_ROOM_IDS,
+ "water_room_ids": WATER_ROOM_IDS,
+ "dont_randomize_room_ids": DONT_RANDOMIZE_ROOM_IDS,
+ "no_special_enemies_standard_room_ids": NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS,
+ "boss_room_ids": BOSS_ROOM_IDS,
+ "room_requirements": tuple(
+ RoomGroupRequirement(
+ group_id=requirement.group_id,
+ subgroup_0=requirement.subgroup_0,
+ subgroup_1=requirement.subgroup_1,
+ subgroup_2=requirement.subgroup_2,
+ subgroup_3=requirement.subgroup_3,
+ rooms=requirement.rooms,
+ )
+ for requirement in ROOM_GROUP_REQUIREMENTS
+ ),
+ }
+
+
+def _load_dungeon_sprite_metadata() -> dict[str, object]:
+ return {
+ "room_sprite_id_addresses": {
+ room.room_id: room.sprite_id_addresses
+ for room in DUNGEON_SPRITE_ADDRESSES
+ },
+ "keyed_sprite_id_addresses": KEYED_SPRITE_ID_ADDRESSES,
+ }
+
+
+def _load_enemy_sprite_requirements() -> tuple[EnemySpriteRequirement, ...]:
+ return tuple(
+ EnemySpriteRequirement(
+ sprite_name=entry.sprite_name,
+ sprite_id=entry.sprite_id,
+ boss=entry.boss,
+ overlord=entry.overlord,
+ do_not_randomize=entry.do_not_randomize,
+ killable=entry.killable,
+ npc=entry.npc,
+ never_use_dungeon=entry.never_use_dungeon,
+ never_use_overworld=entry.never_use_overworld,
+ cannot_have_key=entry.cannot_have_key,
+ is_object=entry.is_object,
+ absorbable=entry.absorbable,
+ is_water_sprite=entry.is_water_sprite,
+ is_enemy_sprite=entry.is_enemy_sprite,
+ group_ids=entry.group_ids,
+ subgroup_0=entry.subgroup_0,
+ subgroup_1=entry.subgroup_1,
+ subgroup_2=entry.subgroup_2,
+ subgroup_3=entry.subgroup_3,
+ parameters=entry.parameters,
+ special_glitched=entry.special_glitched,
+ excluded_rooms=entry.excluded_rooms,
+ dont_randomize_rooms=entry.dont_randomize_rooms,
+ spawnable_rooms=entry.spawnable_rooms,
+ )
+ for entry in ENEMY_SPRITE_REQUIREMENTS
+ )
+
+
+def _load_overworld_enemy_metadata() -> dict[str, object]:
+ return {
+ "area_ids": AREA_IDS,
+ "do_not_randomize_area_ids": DO_NOT_RANDOMIZE_AREA_IDS,
+ "forced_group_requirements": tuple(
+ OverworldGroupRequirement(
+ group_id=requirement.group_id,
+ subgroup_0=requirement.subgroup_0,
+ subgroup_1=requirement.subgroup_1,
+ subgroup_2=requirement.subgroup_2,
+ subgroup_3=requirement.subgroup_3,
+ areas=requirement.areas,
+ )
+ for requirement in FORCED_GROUP_REQUIREMENTS
+ ),
+ }
+
+
+@dataclass(frozen=True)
+class MergedRoomRequirement:
+ group_id: Optional[int]
+ subgroup_0: tuple[int, ...]
+ subgroup_1: tuple[int, ...]
+ subgroup_2: tuple[int, ...]
+ subgroup_3: tuple[int, ...]
+
+
+def _merge_room_requirements(room_id: int, room_requirements: tuple[RoomGroupRequirement, ...]) -> MergedRoomRequirement:
+ group_id: Optional[int] = None
+ subgroup_0: list[int] = []
+ subgroup_1: list[int] = []
+ subgroup_2: list[int] = []
+ subgroup_3: list[int] = []
+
+ for requirement in room_requirements:
+ if room_id not in requirement.rooms:
+ continue
+ if requirement.group_id is not None:
+ group_id = requirement.group_id
+ if requirement.subgroup_0 is not None:
+ subgroup_0.append(requirement.subgroup_0)
+ if requirement.subgroup_1 is not None:
+ subgroup_1.append(requirement.subgroup_1)
+ if requirement.subgroup_2 is not None:
+ subgroup_2.append(requirement.subgroup_2)
+ if requirement.subgroup_3 is not None:
+ subgroup_3.append(requirement.subgroup_3)
+
+ return MergedRoomRequirement(
+ group_id=group_id,
+ subgroup_0=tuple(subgroup_0),
+ subgroup_1=tuple(subgroup_1),
+ subgroup_2=tuple(subgroup_2),
+ subgroup_3=tuple(subgroup_3),
+ )
+
+
+def get_room_do_not_update_requirements(state: EnemyShuffleState, room: DungeonEnemyRoom) -> tuple[EnemySpriteRequirement, ...]:
+ room_sprite_ids = {sprite.sprite_id for sprite in room.sprites}
+ return tuple(
+ requirement for requirement in state.sprite_requirements
+ if (requirement.do_not_randomize or room.room_id in requirement.dont_randomize_rooms)
+ and requirement.sprite_id in room_sprite_ids
+ and can_spawn_in_room(requirement, room)
+ )
+
+
+def get_possible_dungeon_sprite_groups(state: EnemyShuffleState, room: DungeonEnemyRoom) -> tuple[DungeonSpriteGroup, ...]:
+ do_not_update = get_room_do_not_update_requirements(state, room)
+ usable_groups = tuple(
+ group for group in state.sprite_groups.values()
+ if 0 < group.dungeon_group_id < 60
+ and _get_possible_enemy_requirements_for_group(state, room, group)
+ )
+ needs_key = any(sprite.has_key for sprite in room.sprites)
+ needs_killable = room.is_shutter_room
+ needs_water = room.is_water_room
+ room_requirements = _get_requirements_for_usable_dungeon_enemies(state)
+ water_requirements = tuple(requirement for requirement in room_requirements if requirement.is_water_sprite)
+ killable_requirements = tuple(
+ requirement for requirement in state.sprite_requirements
+ if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
+ )
+ key_requirements = tuple(requirement for requirement in killable_requirements if not requirement.cannot_have_key)
+
+ if (
+ not needs_key and not needs_killable and not needs_water
+ and not do_not_update
+ and room.required_group_id is None
+ and not room.required_subgroup_0
+ and not room.required_subgroup_1
+ and not room.required_subgroup_2
+ and not room.required_subgroup_3
+ ):
+ return _get_unconstrained_possible_dungeon_sprite_groups(usable_groups, room_requirements, water_requirements)
+
+ return tuple(
+ group for group in usable_groups
+ if (
+ (not do_not_update or _build_requirement_group_matcher(do_not_update)(group))
+ and _group_matches_room_requirement(group, room)
+ and (
+ lambda possible_requirements: (
+ (not needs_killable or any(
+ _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
+ for requirement in _filter_requirements_for_room_water_state(room, possible_requirements)
+ ))
+ and (not needs_key or any(
+ _is_effectively_killable(requirement)
+ and not requirement.cannot_have_key
+ and requirement.sprite_id != STAL_SPRITE_ID
+ for requirement in _filter_requirements_for_room_water_state(room, possible_requirements)
+ ))
+ and (not needs_water or any(
+ requirement.is_water_sprite
+ for requirement in _filter_requirements_for_room_water_state(room, possible_requirements)
+ ))
+ )
+ )(_get_possible_enemy_requirements_for_group(state, room, group))
+ )
+ )
+
+
+def can_spawn_in_room(requirement: EnemySpriteRequirement, room: DungeonEnemyRoom) -> bool:
+ return (
+ room.room_id not in requirement.excluded_rooms
+ and (not requirement.spawnable_rooms or room.room_id in requirement.spawnable_rooms)
+ and (requirement.sprite_id != WALLMASTER_SPRITE_ID or room.room_id < 0x100)
+ )
+
+
+def _get_requirements_for_usable_dungeon_enemies(state: EnemyShuffleState) -> tuple[EnemySpriteRequirement, ...]:
+ return tuple(
+ requirement for requirement in state.sprite_requirements
+ if not requirement.npc
+ and requirement.is_enemy_sprite
+ and not requirement.boss
+ and not requirement.overlord
+ and not requirement.is_object
+ and not requirement.absorbable
+ and not requirement.never_use_dungeon
+ )
+
+
+def _get_requirements_for_usable_overworld_enemies(state: EnemyShuffleState) -> tuple[EnemySpriteRequirement, ...]:
+ return tuple(
+ requirement for requirement in state.sprite_requirements
+ if not requirement.npc
+ and requirement.is_enemy_sprite
+ and not requirement.boss
+ and not requirement.overlord
+ and not requirement.is_object
+ and not requirement.absorbable
+ and not requirement.never_use_overworld
+ )
+
+
+def _filter_requirements_for_room_water_state(
+ room: DungeonEnemyRoom,
+ requirements: tuple[EnemySpriteRequirement, ...],
+) -> tuple[EnemySpriteRequirement, ...]:
+ if room.is_water_room:
+ return tuple(requirement for requirement in requirements if requirement.is_water_sprite)
+ return tuple(requirement for requirement in requirements if not requirement.is_water_sprite)
+
+
+def _is_effectively_killable(requirement: EnemySpriteRequirement) -> bool:
+ return requirement.killable or requirement.sprite_id == WATER_TEKTITE_SPRITE_ID
+
+
+def _get_effectively_killable_sprite_ids(requirements: tuple[EnemySpriteRequirement, ...]) -> set[int]:
+ return {
+ requirement.sprite_id for requirement in requirements
+ if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
+ }
+
+
+def _get_unconstrained_possible_dungeon_sprite_groups(
+ usable_groups: tuple[DungeonSpriteGroup, ...],
+ room_requirements: tuple[EnemySpriteRequirement, ...],
+ water_requirements: tuple[EnemySpriteRequirement, ...],
+) -> tuple[DungeonSpriteGroup, ...]:
+ water_subgroup_3 = set(_flatten_requirement_values(water_requirements, "subgroup_3"))
+ included_group_ids = set(_flatten_requirement_values(room_requirements, "group_ids"))
+ included_subgroup_0 = set(_flatten_requirement_values(room_requirements, "subgroup_0"))
+ included_subgroup_1 = set(_flatten_requirement_values(room_requirements, "subgroup_1"))
+ included_subgroup_2 = set(_flatten_requirement_values(room_requirements, "subgroup_2"))
+ included_subgroup_3 = {
+ subgroup for subgroup in _flatten_requirement_values(room_requirements, "subgroup_3")
+ if subgroup not in water_subgroup_3 and subgroup not in {54, 80}
+ }
+
+ return tuple(
+ group for group in usable_groups
+ if group.group_id in included_group_ids
+ or group.subgroup_0 in included_subgroup_0
+ or group.subgroup_1 in included_subgroup_1
+ or group.subgroup_2 in included_subgroup_2
+ or group.subgroup_3 in included_subgroup_3
+ )
+
+
+def _build_requirement_group_matcher(requirements: tuple[EnemySpriteRequirement, ...]):
+ allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids"))
+ allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0"))
+ allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1"))
+ allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2"))
+ allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3"))
+
+ def matches(group: DungeonSpriteGroup) -> bool:
+ return (
+ not allowed_group_ids or group.group_id in allowed_group_ids
+ ) and (
+ not allowed_subgroup_0 or group.subgroup_0 in allowed_subgroup_0
+ ) and (
+ not allowed_subgroup_1 or group.subgroup_1 in allowed_subgroup_1
+ ) and (
+ not allowed_subgroup_2 or group.subgroup_2 in allowed_subgroup_2
+ ) and (
+ not allowed_subgroup_3 or group.subgroup_3 in allowed_subgroup_3
+ )
+
+ return matches
+
+
+def _build_overworld_requirement_group_matcher(requirements: tuple[EnemySpriteRequirement, ...]):
+ allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids"))
+ allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0"))
+ allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1"))
+ allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2"))
+ allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3"))
+
+ def matches(group: DungeonSpriteGroup) -> bool:
+ return (
+ not allowed_group_ids or group.group_id in allowed_group_ids
+ ) and (
+ not allowed_subgroup_0 or group.subgroup_0 in allowed_subgroup_0
+ ) and (
+ not allowed_subgroup_1 or group.subgroup_1 in allowed_subgroup_1
+ ) and (
+ not allowed_subgroup_2 or group.subgroup_2 in allowed_subgroup_2
+ ) and (
+ not allowed_subgroup_3 or group.subgroup_3 in allowed_subgroup_3
+ )
+
+ return matches
+
+
+def _build_requirement_group_presence_matcher(requirements: tuple[EnemySpriteRequirement, ...]):
+ allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids"))
+ allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0"))
+ allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1"))
+ allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2"))
+ allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3"))
+
+ def matches(group: DungeonSpriteGroup) -> bool:
+ return (
+ group.group_id in allowed_group_ids
+ or group.subgroup_0 in allowed_subgroup_0
+ or group.subgroup_1 in allowed_subgroup_1
+ or group.subgroup_2 in allowed_subgroup_2
+ or group.subgroup_3 in allowed_subgroup_3
+ )
+
+ return matches
+
+
+def _flatten_requirement_values(requirements: tuple[EnemySpriteRequirement, ...], attribute: str) -> tuple[int, ...]:
+ return tuple(
+ value
+ for requirement in requirements
+ for value in getattr(requirement, attribute)
+ )
+
+
+def _group_matches_room_requirement(group: DungeonSpriteGroup, room: DungeonEnemyRoom) -> bool:
+ return (
+ (room.required_group_id is None or room.required_group_id == group.dungeon_group_id)
+ and (not room.required_subgroup_0 or group.subgroup_0 in room.required_subgroup_0)
+ and (not room.required_subgroup_1 or group.subgroup_1 in room.required_subgroup_1)
+ and (not room.required_subgroup_2 or group.subgroup_2 in room.required_subgroup_2)
+ and (not room.required_subgroup_3 or group.subgroup_3 in room.required_subgroup_3)
+ )
+
+
+def get_overworld_do_not_update_requirements(
+ state: EnemyShuffleState,
+ area: OverworldEnemyArea,
+) -> tuple[EnemySpriteRequirement, ...]:
+ area_sprite_ids = {sprite.sprite_id for sprite in area.sprites}
+ return tuple(
+ requirement for requirement in state.sprite_requirements
+ if requirement.do_not_randomize and requirement.sprite_id in area_sprite_ids
+ )
+
+
+def get_possible_overworld_sprite_groups(
+ state: EnemyShuffleState,
+ area: OverworldEnemyArea,
+) -> tuple[DungeonSpriteGroup, ...]:
+ usable_groups = tuple(
+ group for group in state.sprite_groups.values()
+ if 0 < group.group_id < 0x40
+ and _get_possible_enemy_requirements_for_overworld_group(state, group)
+ )
+ do_not_update = get_overworld_do_not_update_requirements(state, area)
+ if not do_not_update:
+ return usable_groups
+
+ do_not_update_matcher = _build_overworld_requirement_group_matcher(do_not_update)
+ return tuple(group for group in usable_groups if do_not_update_matcher(group))
+
+
+def _get_possible_enemy_requirements_for_group(
+ state: EnemyShuffleState,
+ room: DungeonEnemyRoom,
+ group: DungeonSpriteGroup,
+) -> tuple[EnemySpriteRequirement, ...]:
+ dungeon_requirements = _get_requirements_for_usable_dungeon_enemies(state)
+ return tuple(
+ requirement for requirement in dungeon_requirements
+ if can_spawn_in_room(requirement, room)
+ and (
+ not requirement.group_ids or group.dungeon_group_id in requirement.group_ids
+ )
+ and (not requirement.subgroup_0 or group.subgroup_0 in requirement.subgroup_0)
+ and (not requirement.subgroup_1 or group.subgroup_1 in requirement.subgroup_1)
+ and (not requirement.subgroup_2 or group.subgroup_2 in requirement.subgroup_2)
+ and (not requirement.subgroup_3 or group.subgroup_3 in requirement.subgroup_3)
+ )
+
+
+def _get_randomizable_sprites_in_room(
+ state: EnemyShuffleState,
+ room: DungeonEnemyRoom,
+) -> tuple[DungeonEnemySprite, ...]:
+ randomizable_sprite_ids = {
+ requirement.sprite_id for requirement in state.sprite_requirements
+ if not requirement.do_not_randomize and room.room_id not in requirement.dont_randomize_rooms
+ }
+ return tuple(sprite for sprite in room.sprites if sprite.sprite_id in randomizable_sprite_ids)
+
+
+def _get_possible_enemy_requirements_for_overworld_group(
+ state: EnemyShuffleState,
+ group: DungeonSpriteGroup,
+) -> tuple[EnemySpriteRequirement, ...]:
+ overworld_requirements = _get_requirements_for_usable_overworld_enemies(state)
+ return tuple(
+ requirement for requirement in overworld_requirements
+ if (
+ not requirement.group_ids or group.group_id in requirement.group_ids
+ )
+ and (not requirement.subgroup_0 or group.subgroup_0 in requirement.subgroup_0)
+ and (not requirement.subgroup_1 or group.subgroup_1 in requirement.subgroup_1)
+ and (not requirement.subgroup_2 or group.subgroup_2 in requirement.subgroup_2)
+ and (not requirement.subgroup_3 or group.subgroup_3 in requirement.subgroup_3)
+ )
+
+
+def _get_randomizable_sprites_in_overworld_area(
+ state: EnemyShuffleState,
+ area: OverworldEnemyArea,
+) -> tuple[OverworldEnemySprite, ...]:
+ randomizable_sprite_ids = {
+ requirement.sprite_id for requirement in state.sprite_requirements
+ if not requirement.do_not_randomize
+ }
+ return tuple(sprite for sprite in area.sprites if sprite.sprite_id in randomizable_sprite_ids)
+
+
+def _randomize_dungeon_rooms(
+ world: "ALTTPWorld",
+ dungeon_rooms: dict[int, DungeonEnemyRoom],
+ sprite_groups: dict[int, DungeonSpriteGroup],
+ sprite_requirements: tuple[EnemySpriteRequirement, ...],
+) -> dict[int, RandomizedDungeonEnemyRoom]:
+ state = EnemyShuffleState(
+ dungeon_rooms=dungeon_rooms,
+ overworld_areas={},
+ sprite_groups=sprite_groups,
+ sprite_requirements=sprite_requirements,
+ room_group_requirements=tuple(),
+ overworld_group_requirements=tuple(),
+ shutter_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.is_shutter_room),
+ water_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.is_water_room),
+ dont_randomize_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.do_not_randomize),
+ no_special_enemies_standard_room_ids=frozenset(
+ room.room_id for room in dungeon_rooms.values() if room.no_special_enemies_standard
+ ),
+ boss_room_ids=frozenset(),
+ dont_randomize_overworld_area_ids=frozenset(),
+ randomized_dungeon_rooms={},
+ randomized_overworld_areas={},
+ )
+ randomized_rooms: dict[int, RandomizedDungeonEnemyRoom] = {}
+
+ for room_id in sorted(dungeon_rooms):
+ room = dungeon_rooms[room_id]
+ skip_randomization = room.do_not_randomize or (
+ world.options.mode == "standard" and room.no_special_enemies_standard
+ )
+
+ selected_group = sprite_groups.get(room.graphics_block_id + 0x40)
+ if not skip_randomization:
+ possible_groups = get_possible_dungeon_sprite_groups(state, room)
+ if possible_groups:
+ selected_group = world.random.choice(possible_groups)
+
+ if selected_group is None:
+ selected_group = sprite_groups[room.graphics_block_id + 0x40]
+
+ randomized_rooms[room_id] = _randomize_room_sprites(
+ world,
+ state,
+ room,
+ selected_group,
+ skip_randomization,
+ )
+
+ return randomized_rooms
+
+
+def _randomize_overworld_areas(
+ world: "ALTTPWorld",
+ overworld_areas: dict[int, OverworldEnemyArea],
+ sprite_groups: dict[int, DungeonSpriteGroup],
+ sprite_requirements: tuple[EnemySpriteRequirement, ...],
+ forced_group_requirements: tuple[OverworldGroupRequirement, ...],
+) -> dict[int, RandomizedOverworldEnemyArea]:
+ state = EnemyShuffleState(
+ dungeon_rooms={},
+ overworld_areas=overworld_areas,
+ sprite_groups=sprite_groups,
+ sprite_requirements=sprite_requirements,
+ room_group_requirements=tuple(),
+ overworld_group_requirements=forced_group_requirements,
+ shutter_room_ids=frozenset(),
+ water_room_ids=frozenset(),
+ dont_randomize_room_ids=frozenset(),
+ no_special_enemies_standard_room_ids=frozenset(),
+ boss_room_ids=frozenset(),
+ dont_randomize_overworld_area_ids=frozenset(area.area_id for area in overworld_areas.values() if area.do_not_randomize),
+ randomized_dungeon_rooms={},
+ randomized_overworld_areas={},
+ )
+ randomized_areas: dict[int, RandomizedOverworldEnemyArea] = {}
+
+ for area_id in sorted(overworld_areas):
+ area = overworld_areas[area_id]
+ selected_group = sprite_groups.get(area.graphics_block_id)
+ if not area.do_not_randomize:
+ possible_groups = get_possible_overworld_sprite_groups(state, area)
+ if possible_groups:
+ selected_group = world.random.choice(possible_groups)
+
+ forced_group = _get_forced_overworld_group(area.area_id, forced_group_requirements, sprite_groups)
+ if forced_group is not None:
+ selected_group = forced_group
+ if selected_group is None:
+ selected_group = sprite_groups[area.graphics_block_id]
+
+ randomized_areas[area_id] = _randomize_overworld_area_sprites(
+ world,
+ state,
+ area,
+ selected_group,
+ area.do_not_randomize,
+ )
+
+ return randomized_areas
+
+
+def _get_forced_overworld_group(
+ area_id: int,
+ forced_group_requirements: tuple[OverworldGroupRequirement, ...],
+ sprite_groups: dict[int, DungeonSpriteGroup],
+) -> Optional[DungeonSpriteGroup]:
+ for requirement in forced_group_requirements:
+ if area_id not in requirement.areas or requirement.group_id is None:
+ continue
+ return sprite_groups.get(requirement.group_id)
+ return None
+
+
+def _randomize_room_sprites(
+ world: "ALTTPWorld",
+ state: EnemyShuffleState,
+ room: DungeonEnemyRoom,
+ selected_group: DungeonSpriteGroup,
+ skip_randomization: bool,
+) -> RandomizedDungeonEnemyRoom:
+ randomized_sprites = list(_clone_room_sprites(room))
+
+ if not skip_randomization:
+ possible_requirements = _get_possible_enemy_requirements_for_group(state, room, selected_group)
+ sprites_to_update = _get_randomizable_sprites_in_room(state, room)
+ sprites_to_update_addresses = {sprite.address for sprite in sprites_to_update}
+
+ if possible_requirements:
+ water_sprite_ids = [
+ requirement.sprite_id for requirement in possible_requirements
+ if requirement.is_water_sprite
+ ]
+
+ if room.is_water_room:
+ if water_sprite_ids:
+ replacement_water_sprite_ids = water_sprite_ids
+ if room.is_shutter_room:
+ killable_water_sprite_ids = [
+ requirement.sprite_id for requirement in possible_requirements
+ if requirement.is_water_sprite
+ and _is_effectively_killable(requirement)
+ and requirement.sprite_id != STAL_SPRITE_ID
+ ]
+ if killable_water_sprite_ids:
+ replacement_water_sprite_ids = killable_water_sprite_ids
+ for sprite in randomized_sprites:
+ if sprite.address in sprites_to_update_addresses:
+ _set_randomized_sprite_id(
+ randomized_sprites,
+ sprite.address,
+ world.random.choice(replacement_water_sprite_ids),
+ )
+ return _build_randomized_room(room, selected_group, randomized_sprites, False)
+
+ non_water_requirements = _filter_requirements_for_room_water_state(room, possible_requirements)
+ possible_sprite_ids = [requirement.sprite_id for requirement in non_water_requirements]
+ if not possible_sprite_ids:
+ return _build_randomized_room(room, selected_group, randomized_sprites, False)
+ killable_sprite_ids = [
+ requirement.sprite_id for requirement in non_water_requirements
+ if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
+ ]
+ killable_key_sprite_ids = [
+ requirement.sprite_id for requirement in non_water_requirements
+ if _is_effectively_killable(requirement) and not requirement.cannot_have_key and requirement.sprite_id != STAL_SPRITE_ID
+ ]
+ stal_count = 0
+
+ for sprite in sprites_to_update:
+ replacement_sprite_id: int
+ if sprite.has_key and killable_key_sprite_ids:
+ replacement_sprite_id = world.random.choice(killable_key_sprite_ids)
+ elif room.is_shutter_room and killable_sprite_ids:
+ replacement_sprite_id = world.random.choice(killable_sprite_ids)
+ elif not room.is_shutter_room and world.random.randrange(100) < 5:
+ replacement_sprite_id = STAL_SPRITE_ID
+ else:
+ replacement_sprite_id = world.random.choice(possible_sprite_ids)
+
+ _set_randomized_sprite_id(randomized_sprites, sprite.address, replacement_sprite_id)
+
+ if replacement_sprite_id == STAL_SPRITE_ID:
+ stal_count += 1
+ if stal_count > 2:
+ possible_sprite_ids = [sprite_id for sprite_id in possible_sprite_ids if sprite_id != STAL_SPRITE_ID]
+
+ return _build_randomized_room(room, selected_group, randomized_sprites, skip_randomization)
+
+
+def _randomize_overworld_area_sprites(
+ world: "ALTTPWorld",
+ state: EnemyShuffleState,
+ area: OverworldEnemyArea,
+ selected_group: DungeonSpriteGroup,
+ skip_randomization: bool,
+) -> RandomizedOverworldEnemyArea:
+ randomized_sprites = list(_clone_overworld_area_sprites(area))
+ bush_sprite_id = area.bush_sprite_id
+
+ if not skip_randomization:
+ possible_requirements = _get_possible_enemy_requirements_for_overworld_group(state, selected_group)
+ possible_sprite_ids = [requirement.sprite_id for requirement in possible_requirements]
+ sprites_to_update = _get_randomizable_sprites_in_overworld_area(state, area)
+ sprites_to_update_addresses = {sprite.address for sprite in sprites_to_update}
+
+ if possible_sprite_ids:
+ for sprite in sprites_to_update:
+ _set_randomized_overworld_sprite_id(
+ randomized_sprites,
+ sprite.address,
+ world.random.choice(possible_sprite_ids),
+ )
+
+ flopping_fish_addresses = [
+ sprite.address for sprite in randomized_sprites
+ if sprite.address in sprites_to_update_addresses and sprite.sprite_id == FLOPPING_FISH_SPRITE_ID
+ ]
+ if len(flopping_fish_addresses) > 1:
+ non_fish_sprite_ids = [
+ sprite_id for sprite_id in possible_sprite_ids if sprite_id != FLOPPING_FISH_SPRITE_ID
+ ]
+ for address in flopping_fish_addresses[1:]:
+ if non_fish_sprite_ids:
+ _set_randomized_overworld_sprite_id(
+ randomized_sprites,
+ address,
+ world.random.choice(non_fish_sprite_ids),
+ )
+
+ bush_candidates = [
+ requirement.sprite_id for requirement in possible_requirements
+ if not requirement.overlord
+ ]
+ if bush_candidates:
+ bush_sprite_id = world.random.choice(bush_candidates)
+
+ return RandomizedOverworldEnemyArea(
+ area_id=area.area_id,
+ sprite_table_address=area.sprite_table_address,
+ graphics_block_address=area.graphics_block_address,
+ original_graphics_block_id=area.graphics_block_id,
+ graphics_block_id=selected_group.group_id,
+ original_bush_sprite_id=area.bush_sprite_id,
+ bush_sprite_id=bush_sprite_id,
+ sprites=tuple(randomized_sprites),
+ skipped_randomization=skip_randomization,
+ )
+
+
+def _clone_room_sprites(room: DungeonEnemyRoom) -> list[RandomizedDungeonEnemySprite]:
+ return [
+ RandomizedDungeonEnemySprite(
+ address=sprite.address,
+ byte_0=sprite.byte_0,
+ byte_1=sprite.byte_1,
+ original_sprite_id=sprite.sprite_id,
+ sprite_id=sprite.sprite_id,
+ is_overlord=sprite.is_overlord,
+ has_key=sprite.has_key,
+ )
+ for sprite in room.sprites
+ ]
+
+
+def _clone_overworld_area_sprites(area: OverworldEnemyArea) -> list[RandomizedOverworldEnemySprite]:
+ return [
+ RandomizedOverworldEnemySprite(
+ address=sprite.address,
+ y_coord=sprite.y_coord,
+ x_coord=sprite.x_coord,
+ original_sprite_id=sprite.sprite_id,
+ sprite_id=sprite.sprite_id,
+ )
+ for sprite in area.sprites
+ ]
+
+
+def _set_randomized_sprite_id(
+ randomized_sprites: list[RandomizedDungeonEnemySprite],
+ address: int,
+ sprite_id: int,
+) -> None:
+ for index, sprite in enumerate(randomized_sprites):
+ if sprite.address != address:
+ continue
+ randomized_sprites[index] = RandomizedDungeonEnemySprite(
+ address=sprite.address,
+ byte_0=sprite.byte_0,
+ byte_1=sprite.byte_1,
+ original_sprite_id=sprite.original_sprite_id,
+ sprite_id=sprite_id,
+ is_overlord=sprite.is_overlord,
+ has_key=sprite.has_key,
+ )
+ return
+
+
+def _set_randomized_overworld_sprite_id(
+ randomized_sprites: list[RandomizedOverworldEnemySprite],
+ address: int,
+ sprite_id: int,
+) -> None:
+ for index, sprite in enumerate(randomized_sprites):
+ if sprite.address != address:
+ continue
+ randomized_sprites[index] = RandomizedOverworldEnemySprite(
+ address=sprite.address,
+ y_coord=sprite.y_coord,
+ x_coord=sprite.x_coord,
+ original_sprite_id=sprite.original_sprite_id,
+ sprite_id=sprite_id,
+ )
+ return
+
+
+def _build_randomized_room(
+ room: DungeonEnemyRoom,
+ selected_group: DungeonSpriteGroup,
+ sprites: list[RandomizedDungeonEnemySprite],
+ skipped_randomization: bool,
+) -> RandomizedDungeonEnemyRoom:
+ return RandomizedDungeonEnemyRoom(
+ room_id=room.room_id,
+ room_header_address=room.room_header_address,
+ sprite_table_address=room.sprite_table_address,
+ original_graphics_block_id=room.graphics_block_id,
+ graphics_block_id=selected_group.dungeon_group_id,
+ tag_1=room.tag_1,
+ tag_2=room.tag_2,
+ sort_sprites_value=room.sort_sprites_value,
+ sprites=tuple(sprites),
+ skipped_randomization=skipped_randomization,
+ )
+
+
+def validate_enemy_shuffle_state(state: EnemyShuffleState, is_standard_mode: bool) -> None:
+ for room_id, room in state.dungeon_rooms.items():
+ randomized_room = state.randomized_dungeon_rooms[room_id]
+ _validate_dungeon_room(state, room, randomized_room, is_standard_mode)
+
+ for area_id, area in state.overworld_areas.items():
+ randomized_area = state.randomized_overworld_areas[area_id]
+ _validate_overworld_area(state, area, randomized_area)
+
+
+def _validate_dungeon_room(
+ state: EnemyShuffleState,
+ room: DungeonEnemyRoom,
+ randomized_room: RandomizedDungeonEnemyRoom,
+ is_standard_mode: bool,
+) -> None:
+ selected_group = state.sprite_groups.get(randomized_room.graphics_block_id + 0x40)
+ if selected_group is None:
+ raise ValueError(f"Enemy shuffle produced unknown dungeon sprite group {randomized_room.graphics_block_id} for room {room.room_id}")
+
+ skipped = room.do_not_randomize or (is_standard_mode and room.no_special_enemies_standard)
+ if skipped and randomized_room.graphics_block_id != room.graphics_block_id:
+ raise ValueError(f"Enemy shuffle changed skipped room {room.room_id} graphics block")
+
+ if not skipped:
+ possible_groups = get_possible_dungeon_sprite_groups(state, room)
+ if possible_groups and selected_group not in possible_groups:
+ raise ValueError(f"Enemy shuffle selected illegal sprite group {selected_group.group_id} for room {room.room_id}")
+
+ possible_requirements = _get_possible_enemy_requirements_for_group(state, room, selected_group)
+ possible_sprite_ids = {requirement.sprite_id for requirement in possible_requirements}
+ killable_sprite_ids = _get_effectively_killable_sprite_ids(possible_requirements)
+ killable_key_sprite_ids = {
+ requirement.sprite_id for requirement in possible_requirements
+ if _is_effectively_killable(requirement) and not requirement.cannot_have_key and requirement.sprite_id != STAL_SPRITE_ID
+ }
+ water_sprite_ids = {
+ requirement.sprite_id for requirement in possible_requirements
+ if requirement.is_water_sprite
+ }
+ if not room.is_water_room:
+ possible_sprite_ids -= water_sprite_ids
+ killable_sprite_ids -= water_sprite_ids
+ killable_key_sprite_ids -= water_sprite_ids
+ do_not_randomize_sprite_ids = {
+ requirement.sprite_id for requirement in state.sprite_requirements
+ if requirement.do_not_randomize or room.room_id in requirement.dont_randomize_rooms
+ }
+ randomized_by_address = {sprite.address: sprite for sprite in randomized_room.sprites}
+
+ for original_sprite in room.sprites:
+ randomized_sprite = randomized_by_address[original_sprite.address]
+ if original_sprite.sprite_id in do_not_randomize_sprite_ids and randomized_sprite.sprite_id != original_sprite.sprite_id:
+ raise ValueError(f"Enemy shuffle changed do-not-randomize sprite in room {room.room_id} at {hex(original_sprite.address)}")
+
+ if original_sprite.sprite_id in do_not_randomize_sprite_ids or skipped:
+ continue
+
+ if room.is_water_room:
+ if randomized_sprite.sprite_id not in water_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed non-water enemy {hex(randomized_sprite.sprite_id)} in water room {room.room_id}")
+ continue
+ if randomized_sprite.sprite_id in water_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed water enemy {hex(randomized_sprite.sprite_id)} in non-water room {room.room_id}")
+
+ if original_sprite.has_key:
+ if randomized_sprite.sprite_id not in killable_key_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed invalid key enemy {hex(randomized_sprite.sprite_id)} in room {room.room_id}")
+ continue
+
+ if room.is_shutter_room and randomized_sprite.sprite_id not in killable_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed non-killable shutter enemy {hex(randomized_sprite.sprite_id)} in room {room.room_id}")
+
+ if randomized_sprite.sprite_id != STAL_SPRITE_ID and randomized_sprite.sprite_id not in possible_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed illegal sprite {hex(randomized_sprite.sprite_id)} in room {room.room_id}")
+
+ if room.is_shutter_room and _get_randomizable_sprites_in_room(state, room):
+ all_killable_sprite_ids = _get_effectively_killable_sprite_ids(
+ _filter_requirements_for_room_water_state(room, state.sprite_requirements)
+ )
+ randomized_sprite_ids = {sprite.sprite_id for sprite in randomized_room.sprites}
+ if not (randomized_sprite_ids & all_killable_sprite_ids):
+ raise ValueError(f"Enemy shuffle left shutter room {room.room_id} without any killable enemies")
+
+
+def _validate_overworld_area(
+ state: EnemyShuffleState,
+ area: OverworldEnemyArea,
+ randomized_area: RandomizedOverworldEnemyArea,
+) -> None:
+ selected_group = state.sprite_groups.get(randomized_area.graphics_block_id)
+ if selected_group is None:
+ raise ValueError(f"Enemy shuffle produced unknown overworld sprite group {randomized_area.graphics_block_id} for area {hex(area.area_id)}")
+
+ if area.do_not_randomize and randomized_area.graphics_block_id != area.graphics_block_id:
+ raise ValueError(f"Enemy shuffle changed skipped overworld area {hex(area.area_id)} graphics block")
+
+ forced_group = _get_forced_overworld_group(area.area_id, state.overworld_group_requirements, state.sprite_groups)
+ if forced_group is not None and randomized_area.graphics_block_id != forced_group.group_id:
+ raise ValueError(f"Enemy shuffle failed forced overworld group for area {hex(area.area_id)}")
+
+ if not area.do_not_randomize and forced_group is None:
+ possible_groups = get_possible_overworld_sprite_groups(state, area)
+ if possible_groups and selected_group not in possible_groups:
+ raise ValueError(f"Enemy shuffle selected illegal overworld group {selected_group.group_id} for area {hex(area.area_id)}")
+
+ possible_requirements = _get_possible_enemy_requirements_for_overworld_group(state, selected_group)
+ possible_sprite_ids = {requirement.sprite_id for requirement in possible_requirements}
+ bush_sprite_ids = {
+ requirement.sprite_id for requirement in possible_requirements
+ if not requirement.overlord
+ }
+ known_sprite_ids = {requirement.sprite_id for requirement in state.sprite_requirements}
+ do_not_randomize_sprite_ids = {
+ requirement.sprite_id for requirement in state.sprite_requirements
+ if requirement.do_not_randomize
+ }
+ randomized_by_address = {sprite.address: sprite for sprite in randomized_area.sprites}
+
+ for original_sprite in area.sprites:
+ randomized_sprite = randomized_by_address[original_sprite.address]
+ if original_sprite.sprite_id not in known_sprite_ids:
+ continue
+ if original_sprite.sprite_id in do_not_randomize_sprite_ids and randomized_sprite.sprite_id != original_sprite.sprite_id:
+ raise ValueError(f"Enemy shuffle changed do-not-randomize overworld sprite in area {hex(area.area_id)} at {hex(original_sprite.address)}")
+ if original_sprite.sprite_id in do_not_randomize_sprite_ids or area.do_not_randomize:
+ continue
+ if randomized_sprite.sprite_id not in possible_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed illegal overworld sprite {hex(randomized_sprite.sprite_id)} in area {hex(area.area_id)}")
+
+ randomizable_addresses = {sprite.address for sprite in _get_randomizable_sprites_in_overworld_area(state, area)}
+ non_fish_sprite_ids = possible_sprite_ids - {FLOPPING_FISH_SPRITE_ID}
+ if non_fish_sprite_ids and sum(
+ 1 for sprite in randomized_area.sprites
+ if sprite.address in randomizable_addresses and sprite.sprite_id == FLOPPING_FISH_SPRITE_ID
+ ) > 1:
+ raise ValueError(f"Enemy shuffle placed multiple flopping fish in area {hex(area.area_id)}")
+
+ if area.do_not_randomize and randomized_area.bush_sprite_id != area.bush_sprite_id:
+ raise ValueError(f"Enemy shuffle changed skipped overworld bush sprite in area {hex(area.area_id)}")
+ if not area.do_not_randomize and bush_sprite_ids and randomized_area.bush_sprite_id not in bush_sprite_ids:
+ raise ValueError(f"Enemy shuffle placed illegal bush enemy {hex(randomized_area.bush_sprite_id)} in area {hex(area.area_id)}")
+
+
+def apply_enemy_shuffle(rom: "LocalRom", state: EnemyShuffleState) -> None:
+ for group in state.sprite_groups.values():
+ _write_sprite_group(rom, group)
+
+ for room in state.randomized_dungeon_rooms.values():
+ rom.write_byte(room.room_header_address + 3, room.graphics_block_id)
+ for sprite in room.sprites:
+ _write_dungeon_sprite(rom, sprite)
+
+ rom.write_byte(0x04CF4F, 0x10)
+ for area in state.randomized_overworld_areas.values():
+ rom.write_byte(area.graphics_block_address, area.graphics_block_id)
+ for sprite in area.sprites:
+ _write_overworld_sprite(rom, sprite)
+
+ bush_spawn_table_address = _get_enemizer_symbol("sprite_bush_spawn_table_overworld")
+ for area in state.randomized_overworld_areas.values():
+ rom.write_byte(bush_spawn_table_address + area.area_id, area.bush_sprite_id)
+
+
+def _write_sprite_group(rom: "LocalRom", group: DungeonSpriteGroup) -> None:
+ address = SPRITE_GROUP_BASE_ADDRESS + (group.group_id * 4)
+ rom.write_byte(address, group.subgroup_0)
+ rom.write_byte(address + 1, group.subgroup_1)
+ rom.write_byte(address + 2, group.subgroup_2)
+ rom.write_byte(address + 3, group.subgroup_3)
+
+
+def _write_dungeon_sprite(rom: "LocalRom", sprite: RandomizedDungeonEnemySprite) -> None:
+ sprite_id = sprite.sprite_id
+ byte_1 = sprite.byte_1
+
+ if sprite_id == WALLMASTER_SPRITE_ID:
+ sprite_id = 0x09
+ byte_1 |= SPRITE_OVERLORD_MASK
+
+ rom.write_byte(sprite.address, sprite.byte_0)
+ rom.write_byte(sprite.address + 1, byte_1)
+ rom.write_byte(sprite.address + 2, sprite_id & 0xFF)
+
+
+def _write_overworld_sprite(rom: "LocalRom", sprite: RandomizedOverworldEnemySprite) -> None:
+ sprite_id = sprite.sprite_id
+ if sprite_id == OW_FALLING_ROCKS_SPRITE_ID:
+ rom.write_byte(sprite.address, 0)
+ rom.write_byte(sprite.address + 1, 0)
+ if sprite_id == WALLMASTER_SPRITE_ID:
+ sprite_id = OW_WALLMASTER_TO_HOULIHAN_SPRITE_ID
+ rom.write_byte(sprite.address + 2, sprite_id & 0xFF)
diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py
index c7dc7a694848..5b9d443b5dcd 100644
--- a/worlds/alttp/ItemPool.py
+++ b/worlds/alttp/ItemPool.py
@@ -9,6 +9,7 @@
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
from .Bosses import place_bosses
from .Dungeons import get_dungeon_item_pool_player
+from .EnemyShuffle import generate_enemy_shuffle_state
from .EntranceShuffle import connect_entrance
from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups
from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses
@@ -511,6 +512,8 @@ def cut_item(items, item_to_cut, minimum_items):
world.options.turtle_rock_medallion.current_key.title())
place_bosses(world)
+ if world.options.enemy_shuffle:
+ world.enemy_shuffle_state = generate_enemy_shuffle_state(world)
multiworld.itempool += items
diff --git a/worlds/alttp/PotShuffle.py b/worlds/alttp/PotShuffle.py
new file mode 100644
index 000000000000..f1f8825983a2
--- /dev/null
+++ b/worlds/alttp/PotShuffle.py
@@ -0,0 +1,125 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+from Utils import snes_to_pc
+from .enemizer_data.pot_shuffle_data import POT_ROOMS
+
+if TYPE_CHECKING:
+ from . import ALTTPWorld
+ from .Rom import LocalRom
+
+
+POT_ITEM_POINTER_TABLE = 0xDB67
+POT_KEY = 0x08
+POT_ARROW = 0x09
+POT_BLUE_RUPEE = 0x07
+POT_SWITCH = 0x88
+POT_HOLE = 0x80
+
+
+@dataclass(frozen=True)
+class PotData:
+ x: int
+ y: int
+ reserved: int
+
+
+@dataclass(frozen=True)
+class PotRoomData:
+ room_id: int
+ pots: tuple[PotData, ...]
+ items: tuple[int, ...]
+
+
+@dataclass(frozen=True)
+class FilledPot:
+ x: int
+ y: int
+ item: int
+
+
+def generate_pot_shuffle(world: "ALTTPWorld") -> dict[int, tuple[FilledPot, ...]]:
+ room_data = _load_pot_room_data()
+ shuffled_pots: dict[int, tuple[FilledPot, ...]] = {}
+
+ for room in room_data:
+ room_items = [item for item in room.items if item != POT_HOLE]
+ if world.options.retro_bow:
+ room_items = [POT_BLUE_RUPEE if item == POT_ARROW else item for item in room_items]
+
+ empty_pots: list[PotData] = []
+ filled_pots: list[FilledPot] = []
+
+ for pot in room.pots:
+ if pot.reserved == 3:
+ filled_pots.append(FilledPot(pot.x, pot.y, POT_HOLE))
+ else:
+ empty_pots.append(pot)
+
+ while POT_KEY in room_items:
+ candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 1]
+ if not candidate_indices:
+ break
+ pot_index = world.random.choice(candidate_indices)
+ pot = empty_pots.pop(pot_index)
+ room_items.remove(POT_KEY)
+ filled_pots.append(FilledPot(pot.x, pot.y, POT_KEY))
+
+ while POT_SWITCH in room_items:
+ candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 2]
+ if not candidate_indices:
+ break
+ pot_index = world.random.choice(candidate_indices)
+ pot = empty_pots.pop(pot_index)
+ room_items.remove(POT_SWITCH)
+ filled_pots.append(FilledPot(pot.x, pot.y, POT_SWITCH))
+
+ while room_items and empty_pots:
+ pot_index = world.random.randrange(len(empty_pots))
+ item_index = world.random.randrange(len(room_items))
+ pot = empty_pots.pop(pot_index)
+ item = room_items.pop(item_index)
+ filled_pots.append(FilledPot(pot.x, pot.y, item))
+
+ shuffled_pots[room.room_id] = tuple(filled_pots)
+
+ return shuffled_pots
+
+
+def apply_pot_shuffle(rom: "LocalRom", shuffled_pots: dict[int, tuple[FilledPot, ...]]) -> None:
+ for room_id, pots in shuffled_pots.items():
+ pointer_address = POT_ITEM_POINTER_TABLE + (room_id * 2)
+ snes_address = rom.read_byte(pointer_address) | (rom.read_byte(pointer_address + 1) << 8) | (0x01 << 16)
+ address = snes_to_pc(snes_address)
+ for index, pot in enumerate(pots):
+ rom.write_bytes(address + (index * 3), (pot.x, pot.y, pot.item))
+
+
+def get_unique_pot_item_position(
+ shuffled_pots: dict[int, tuple[FilledPot, ...]],
+ room_id: int,
+ item: int,
+) -> tuple[int, int]:
+ positions = [
+ (pot.x, pot.y)
+ for pot in shuffled_pots.get(room_id, ())
+ if pot.item == item
+ ]
+ if len(positions) != 1:
+ raise ValueError(
+ f"Expected exactly one pot item {hex(item)} in room {hex(room_id)}, found {len(positions)}"
+ )
+ return positions[0]
+
+
+def _load_pot_room_data() -> tuple[PotRoomData, ...]:
+ return tuple(
+ PotRoomData(
+ room_id=room.room_id,
+ pots=tuple(PotData(x=pot.x, y=pot.y, reserved=pot.reserved) for pot in room.pots),
+ items=room.items,
+ )
+ for room in POT_ROOMS
+ )
diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py
index 8acff214f24b..78cb33993d92 100644
--- a/worlds/alttp/Rom.py
+++ b/worlds/alttp/Rom.py
@@ -15,7 +15,6 @@
import os
import random
import struct
-import subprocess
import threading
import concurrent.futures
import bsdiff4
@@ -53,9 +52,6 @@
except:
xxtea = None
-enemizer_logger = logging.getLogger("Enemizer")
-
-
class LocalRom:
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
@@ -179,43 +175,6 @@ def write_int32s(self, startaddress: int, values):
self.write_int32(startaddress + (i * 4), value)
-check_lock = threading.Lock()
-
-
-def check_enemizer(enemizercli):
- if getattr(check_enemizer, "done", None):
- return
- if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
- raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
- f"Such as https://github.com/Ijwu/Enemizer/releases")
-
- with check_lock:
- # some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
- if getattr(check_enemizer, "done", None):
- return
- wanted_version = (7, 1, 0)
- # version info is saved on the lib, for some reason
- library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
- with open(library_info) as f:
- info = json.load(f)
-
- for lib in info["libraries"]:
- if lib.startswith("EnemizerLibrary/"):
- version = lib.split("/")[-1]
- version = tuple(int(element) for element in version.split("."))
- enemizer_logger.debug(f"Found Enemizer version {version}")
- if version < wanted_version:
- raise Exception(
- f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
- f"please update your Enemizer. "
- f"Such as from https://github.com/Ijwu/Enemizer/releases")
- break
- else:
- raise Exception(f"Could not find Enemizer library version information in {library_info}")
-
- check_enemizer.done = True
-
-
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
@@ -282,174 +241,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
-
-def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
- player = world.player
- check_enemizer(enemizercli)
- randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
- options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
- enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
-
- # write options file for enemizer
- options = {
- 'RandomizeEnemies': world.options.enemy_shuffle.value,
- 'RandomizeEnemiesType': 3,
- 'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
- 'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
- 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
- world.options.enemy_health.current_key],
- 'OHKO': False,
- 'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
- 'AllowEnemyZeroDamage': True,
- 'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
- 'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
- 'EasyModeEscape': world.options.mode == "standard",
- 'EnemiesAbsorbable': False,
- 'AbsorbableSpawnRate': 10,
- 'AbsorbableTypes': {
- 'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True,
- 'Key': True,
- 'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True,
- 'RedRupee': True
- },
- 'BossMadness': False,
- 'RandomizeBosses': True,
- 'RandomizeBossesType': 0,
- 'RandomizeBossHealth': False,
- 'RandomizeBossHealthMinAmount': 0,
- 'RandomizeBossHealthMaxAmount': 300,
- 'RandomizeBossDamage': False,
- 'RandomizeBossDamageMinAmount': 0,
- 'RandomizeBossDamageMaxAmount': 200,
- 'RandomizeBossBehavior': False,
- 'RandomizeDungeonPalettes': False,
- 'SetBlackoutMode': False,
- 'RandomizeOverworldPalettes': False,
- 'RandomizeSpritePalettes': False,
- 'SetAdvancedSpritePalettes': False,
- 'PukeMode': False,
- 'NegativeMode': False,
- 'GrayscaleMode': False,
- 'GenerateSpoilers': False,
- 'RandomizeLinkSpritePalette': False,
- 'RandomizePots': world.options.pot_shuffle.value,
- 'ShuffleMusic': False,
- 'BootlegMagic': True,
- 'CustomBosses': False,
- 'AndyMode': False,
- 'HeartBeepSpeed': 0,
- 'AlternateGfx': False,
- 'ShieldGraphics': "shield_gfx/normal.gfx",
- 'SwordGraphics': "sword_gfx/normal.gfx",
- 'BeeMizer': False,
- 'BeesLevel': 0,
- 'RandomizeTileTrapPattern': False,
- 'RandomizeTileTrapFloorTile': False,
- 'AllowKillableThief': world.options.killable_thieves.value,
- 'RandomizeSpriteOnHit': False,
- 'DebugMode': False,
- 'DebugForceEnemy': False,
- 'DebugForceEnemyId': 0,
- 'DebugForceBoss': False,
- 'DebugForceBossId': 0,
- 'DebugOpenShutterDoors': False,
- 'DebugForceEnemyDamageZero': False,
- 'DebugShowRoomIdInRupeeCounter': False,
- 'UseManualBosses': True,
- 'ManualBosses': {
- 'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
- 'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
- 'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
- 'AgahnimsTower': 'Agahnim',
- 'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
- 'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
- 'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
- 'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
- 'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
- 'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
- 'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
- 'GanonsTower1':
- world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
- "Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
- 'GanonsTower2':
- world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
- "Inverted Ganons Tower"].bosses['middle'].enemizer_name,
- 'GanonsTower3':
- world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
- "Inverted Ganons Tower"].bosses['top'].enemizer_name,
- 'GanonsTower4': 'Agahnim2',
- 'Ganon': 'Ganon',
- }
- }
-
- rom.write_to_file(randopatch_path)
-
- with open(options_path, 'w') as f:
- json.dump(options, f)
-
- max_enemizer_tries = 5
- for i in range(max_enemizer_tries):
- enemizer_seed = str(world.random.randint(0, 999999999))
- enemizer_command = [os.path.abspath(enemizercli),
- '--rom', randopatch_path,
- '--seed', enemizer_seed,
- '--binary',
- '--enemizer', options_path,
- '--output', enemizer_output_path]
-
- p_open = subprocess.Popen(enemizer_command,
- cwd=os.path.dirname(enemizercli),
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- universal_newlines=True)
-
- enemizer_logger.debug(
- f"Enemizer attempt {i + 1} of {max_enemizer_tries} for player {player} using enemizer seed {enemizer_seed}")
- for stdout_line in iter(p_open.stdout.readline, ""):
- if i == max_enemizer_tries - 1:
- enemizer_logger.warning(stdout_line.rstrip())
- else:
- enemizer_logger.debug(stdout_line.rstrip())
- p_open.stdout.close()
-
- return_code = p_open.wait()
- if return_code:
- if i == max_enemizer_tries - 1:
- raise subprocess.CalledProcessError(return_code, enemizer_command)
- continue
-
- for j in range(i + 1, max_enemizer_tries):
- world.random.randint(0, 999999999)
- # Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
- # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
- break
-
- rom.read_from_file(enemizer_output_path)
- os.remove(enemizer_output_path)
-
- if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
- rom.write_byte(0x04DE81, 6)
- rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
-
- # Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
- # Replace them with a Slime enemy if they are placed.
- if world.options.key_drop_shuffle:
- key_drop_enemies = {
- 0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
- 0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
- }
- for enemy in key_drop_enemies:
- if rom.read_byte(enemy) == 0x12:
- logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
- rom.write_byte(enemy, 0x8F)
-
- for used in (randopatch_path, options_path):
- try:
- os.remove(used)
- except OSError:
- pass
-
-
tile_list_lock = threading.Lock()
_tile_collection_table = []
@@ -795,9 +586,13 @@ def get_nonnative_item_sprite(code: int) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
-def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
+def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
local_random = multiworld.worlds[player].random
local_world = multiworld.worlds[player]
+ enemized = bool(local_world.options.boss_shuffle or local_world.options.enemy_shuffle
+ or local_world.options.enemy_health != 'default' or local_world.options.enemy_damage != 'default'
+ or local_world.options.pot_shuffle or local_world.options.bush_shuffle
+ or local_world.options.killable_thieves)
# patch items
@@ -1331,6 +1126,13 @@ def chunk(l, n):
starting_max_arrows = 30
startingstate = CollectionState(multiworld)
+ has_blue_shield = False
+ has_red_shield = False
+ has_mirror_shield = False
+ progressive_shields = 0
+ has_blue_mail = False
+ has_red_mail = False
+ progressive_mail = 0
if startingstate.has('Silver Bow', player):
equip[0x340] = 1
@@ -1359,18 +1161,6 @@ def chunk(l, n):
elif startingstate.has('Fighter Sword', player):
equip[0x359] = 1
- if startingstate.has('Mirror Shield', player):
- equip[0x35A] = 3
- elif startingstate.has('Red Shield', player):
- equip[0x35A] = 2
- elif startingstate.has('Blue Shield', player):
- equip[0x35A] = 1
-
- if startingstate.has('Red Mail', player):
- equip[0x35B] = 2
- elif startingstate.has('Blue Mail', player):
- equip[0x35B] = 1
-
if startingstate.has('Magic Upgrade (1/4)', player):
equip[0x37B] = 2
equip[0x36E] = 0x80
@@ -1383,8 +1173,6 @@ def chunk(l, n):
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
- 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
- 'Red Mail', 'Blue Mail', 'Progressive Mail',
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
continue
@@ -1489,9 +1277,63 @@ def chunk(l, n):
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
+ elif item.name == 'Blue Shield':
+ has_blue_shield = True
+ continue
+ elif item.name == 'Red Shield':
+ has_red_shield = True
+ continue
+ elif item.name == 'Mirror Shield':
+ has_mirror_shield = True
+ continue
+ elif item.name == 'Progressive Shield':
+ progressive_shields += 1
+ continue
+ elif item.name == 'Blue Mail':
+ has_blue_mail = True
+ continue
+ elif item.name == 'Red Mail':
+ has_red_mail = True
+ continue
+ elif item.name == 'Progressive Mail':
+ progressive_mail += 1
+ continue
else:
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
+ for _ in range(progressive_shields):
+ if has_mirror_shield:
+ continue
+ if has_red_shield and local_world.difficulty_requirements.progressive_shield_limit >= 3:
+ has_mirror_shield = True
+ continue
+ if has_blue_shield and local_world.difficulty_requirements.progressive_shield_limit >= 2:
+ has_red_shield = True
+ continue
+ if local_world.difficulty_requirements.progressive_shield_limit >= 1:
+ has_blue_shield = True
+
+ for _ in range(progressive_mail):
+ if has_red_mail:
+ continue
+ if has_blue_mail and local_world.difficulty_requirements.progressive_armor_limit >= 2:
+ has_red_mail = True
+ continue
+ if local_world.difficulty_requirements.progressive_armor_limit >= 1:
+ has_blue_mail = True
+
+ if has_mirror_shield:
+ equip[0x35A] = 3
+ elif has_red_shield:
+ equip[0x35A] = 2
+ elif has_blue_shield:
+ equip[0x35A] = 1
+
+ if has_red_mail:
+ equip[0x35B] = 2
+ elif has_blue_mail:
+ equip[0x35B] = 1
+
equip[0x343] = min(equip[0x343], starting_max_bombs)
rom.write_byte(0x180034, starting_max_bombs)
equip[0x377] = min(equip[0x377], starting_max_arrows)
@@ -1710,6 +1552,71 @@ def get_reveal_bytes(itemName):
if encoded_players > ROM_PLAYER_LIMIT:
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
+ if enemized:
+ from . import EnemizerPatches as enemizer_patches
+ from .EnemyShuffle import apply_enemy_shuffle
+ from .PotShuffle import apply_pot_shuffle
+
+ enemizer_patches.apply_enemizer_base_patch(rom)
+
+ enemy_shuffle_enabled = bool(local_world.options.enemy_shuffle)
+ bush_shuffle_enabled = bool(local_world.options.bush_shuffle)
+ enemy_health_key = enemizer_patches._option_key(local_world.options.enemy_health)
+ enemy_damage_key = enemizer_patches._option_key(local_world.options.enemy_damage)
+
+ if enemy_shuffle_enabled or bush_shuffle_enabled:
+ enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
+ hidden_enemy_chance_pool = (
+ enemizer_patches.RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL
+ if bush_shuffle_enabled
+ else enemizer_patches.VANILLA_HIDDEN_ENEMY_CHANCE_POOL
+ )
+ rom.write_bytes(enemizer_patches.HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
+ enemizer_patches._update_hidden_enemy_item_table_for_retro_mode(rom)
+
+ if enemy_shuffle_enabled:
+ enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
+ enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
+ enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
+ rom.write_bytes(0x1F2D5, (0x54, 0x9C))
+ rom.write_byte(0x1F2E5, 0xB0)
+ rom.write_byte(0x1F2EB, 0xD0)
+
+ if local_world.options.killable_thieves:
+ enemizer_patches._apply_killable_thief(rom)
+
+ if enemy_health_key != "default" or enemy_damage_key != "default":
+ rng = enemizer_patches._make_native_enemizer_rng(local_world)
+ else:
+ rng = None
+
+ if enemy_health_key != "default":
+ assert rng is not None
+ enemizer_patches._randomize_enemy_health(rom, rng, enemy_health_key)
+
+ if enemy_damage_key != "default":
+ assert rng is not None
+ enemizer_patches._randomize_enemy_damage(rom, rng, allow_zero_damage=True)
+ enemizer_patches._shuffle_damage_groups(
+ rom,
+ rng,
+ chaos_mode=enemy_damage_key == "chaos",
+ allow_zero_damage=True,
+ )
+
+ enemy_shuffle_state = getattr(local_world, "enemy_shuffle_state", None)
+ if local_world.options.enemy_shuffle and enemy_shuffle_state is not None:
+ apply_enemy_shuffle(rom, enemy_shuffle_state)
+
+ if local_world.options.boss_shuffle:
+ # Boss shuffle must run after enemy shuffle so boss room sprite pointers
+ # and graphics block IDs are not restored to the enemy-shuffled room values.
+ enemizer_patches.patch_bosses(local_world, rom)
+
+ pot_shuffle_state = getattr(local_world, "pot_shuffle_state", None)
+ if local_world.options.pot_shuffle and pot_shuffle_state is not None:
+ apply_pot_shuffle(rom, pot_shuffle_state)
+
# Write title screen Code
hashint = int(rom.get_hash(), 16)
code = [
@@ -1860,7 +1767,7 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
- # Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
+ # Preserve SPC $3188 instead of writing the unused "WHAT" sound effect there.
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py
index 2b99162837da..187ce6791888 100644
--- a/worlds/alttp/__init__.py
+++ b/worlds/alttp/__init__.py
@@ -14,9 +14,10 @@
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import ALTTPOptions, small_key_shuffle
+from .PotShuffle import generate_pot_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
is_main_entrance, key_drop_data
-from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
+from .Rom import LocalRom, patch_rom, patch_race_rom, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
@@ -253,17 +254,6 @@ class ALTTPWorld(World):
create_items = generate_itempool
- _enemizer_path: typing.ClassVar[typing.Optional[str]] = None
-
- @property
- def enemizer_path(self) -> str:
- # TODO: directly use settings
- cls = self.__class__
- if cls._enemizer_path is None:
- cls._enemizer_path = settings.get_settings().generator.enemizer_path
- assert isinstance(cls._enemizer_path, str)
- return cls._enemizer_path
-
# custom instance vars
dungeon_local_item_names: typing.Set[str]
dungeon_specific_item_names: typing.Set[str]
@@ -305,6 +295,8 @@ def __init__(self, *args, **kwargs):
self.required_medallions = ["Ether", "Quake"]
self.escape_assist = []
self.shops = []
+ self.enemy_shuffle_state = None
+ self.pot_shuffle_state = None
self.logical_heart_containers = 10
self.logical_heart_pieces = 24
super(ALTTPWorld, self).__init__(*args, **kwargs)
@@ -316,10 +308,6 @@ def stage_assert_generate(cls, multiworld: MultiWorld):
raise FileNotFoundError(rom_file)
if multiworld.is_race:
import xxtea # noqa
- for player in multiworld.get_game_players(cls.game):
- if multiworld.worlds[player].use_enemizer:
- check_enemizer(multiworld.worlds[player].enemizer_path)
- break
def generate_early(self):
multiworld = self.multiworld
@@ -339,6 +327,9 @@ def generate_early(self):
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
+ if self.options.pot_shuffle:
+ self.pot_shuffle_state = generate_pot_shuffle(self)
+
if self.options.mode == 'standard':
if self.options.small_key_shuffle:
if (self.options.small_key_shuffle not in
@@ -564,13 +555,6 @@ def stage_pre_fill(cls, world):
def stage_generate_output(cls, multiworld, output_directory):
push_shop_inventories(multiworld)
- @property
- def use_enemizer(self) -> bool:
- return bool(self.options.boss_shuffle or self.options.enemy_shuffle
- or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
- or self.options.pot_shuffle or self.options.bush_shuffle
- or self.options.killable_thieves)
-
def generate_output(self, output_directory: str):
multiworld = self.multiworld
player = self.player
@@ -578,14 +562,9 @@ def generate_output(self, output_directory: str):
self.pushed_shop_inventories.wait()
try:
- use_enemizer = self.use_enemizer
-
rom = LocalRom(get_base_rom_path())
- patch_rom(multiworld, rom, player, use_enemizer)
-
- if use_enemizer:
- patch_enemizer(self, rom, self.enemizer_path, output_directory)
+ patch_rom(multiworld, rom, player)
if multiworld.is_race:
patch_race_rom(rom, multiworld, player)
diff --git a/worlds/alttp/enemizer_data/README.md b/worlds/alttp/enemizer_data/README.md
new file mode 100644
index 000000000000..f2e9371ead90
--- /dev/null
+++ b/worlds/alttp/enemizer_data/README.md
@@ -0,0 +1,27 @@
+These modules are vendored/generated from the upstream Enemizer compiled release and source that were already present
+locally in `/home/alchav/PycharmProjects/Archipelago/EnemizerCLI` and `/home/alchav/PycharmProjects/Archipelago/Enemizer`.
+
+Source details:
+
+- Upstream project: `Ijwu/Enemizer`
+- Release family: `7.1`
+- Library version from `EnemizerCLI/EnemizerCLI.Core.deps.json`: `EnemizerLibrary/7.1.0`
+
+Vendored data modules:
+
+- `base_patch_data.py`
+- `symbols.py`
+- `enemy_room_metadata.py`
+- `enemy_sprite_requirements.py`
+- `overworld_enemy_metadata.py`
+- `dungeon_sprite_addresses.py`
+- `pot_shuffle_data.py`
+
+Purpose:
+
+- `base_patch_data.py` contains the generated base patch Enemizer applies before feature-specific randomization.
+- `symbols.py` contains the assembled symbol map consumed by Enemizer's runtime code for ROM addresses.
+- `enemy_room_metadata.py` and `overworld_enemy_metadata.py` contain room and area grouping/randomization constraints.
+- `enemy_sprite_requirements.py` contains the sprite metadata used by the native enemy shuffle implementation.
+- `dungeon_sprite_addresses.py` contains dungeon sprite slot metadata derived from Enemizer's source tables and keyed-enemy address list.
+- `pot_shuffle_data.py` contains the native pot shuffle room/item source data.
diff --git a/worlds/alttp/enemizer_data/__init__.py b/worlds/alttp/enemizer_data/__init__.py
new file mode 100644
index 000000000000..7a18008b018a
--- /dev/null
+++ b/worlds/alttp/enemizer_data/__init__.py
@@ -0,0 +1 @@
+"""Native ALTTP Enemizer data modules."""
diff --git a/worlds/alttp/enemizer_data/base_patch_data.py b/worlds/alttp/enemizer_data/base_patch_data.py
new file mode 100644
index 000000000000..5fd784539340
--- /dev/null
+++ b/worlds/alttp/enemizer_data/base_patch_data.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class BasePatchData(NamedTuple):
+ address: int
+ patch_data: bytes
+
+ENEMIZER_BASE_PATCHES = (
+ BasePatchData(address=208, patch_data=b'\\l\x956'),
+ BasePatchData(address=2304, patch_data=b'k'),
+ BasePatchData(address=2317, patch_data=b'"\x92\x9a6'),
+ BasePatchData(address=2335, patch_data=b'"\x98\x9a6'),
+ BasePatchData(address=65816, patch_data=b'"\x87\x9a6\xea\xea'),
+ BasePatchData(address=66134, patch_data=b'"C\x976'),
+ BasePatchData(address=67657, patch_data=b'"1\x976'),
+ BasePatchData(address=67961, patch_data=b'"C\x976'),
+ BasePatchData(address=68630, patch_data=b'"C\x976'),
+ BasePatchData(address=70456, patch_data=b'"C\x976'),
+ BasePatchData(address=192354, patch_data=b'"\xbe\x996'),
+ BasePatchData(address=197241, patch_data=b'\xea\xea\xea\xea\xea\xea\xea\xea\xea\xea"\xf5\x946\xea'),
+ BasePatchData(address=198603, patch_data=b'\xb6\x91'),
+ BasePatchData(address=201142, patch_data=b'"\x1a\x9a6'),
+ BasePatchData(address=224264, patch_data=b'"N\x9a6\xea'),
+ BasePatchData(address=224678, patch_data=b'"f\x9a6'),
+ BasePatchData(address=229578, patch_data=b'"\xb2\x996\xea\xea\xea\xd0$\xea\xea'),
+ BasePatchData(address=312587, patch_data=b'"S\xb76\xea\xb7\x00'),
+ BasePatchData(address=312600, patch_data=b'\xb7\x00'),
+ BasePatchData(address=312616, patch_data=b'\xb7\x00'),
+ BasePatchData(address=312625, patch_data=b'\x85\n'),
+ BasePatchData(address=312628, patch_data=b'\xb7\x00'),
+ BasePatchData(address=312635, patch_data=b'e\n'),
+ BasePatchData(address=312646, patch_data=b'\xb7\x00'),
+ BasePatchData(address=312655, patch_data=b'\xb7\x00'),
+ BasePatchData(address=317847, patch_data=b'\x00\x07\x07\x8c\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\xff\x00\t\t\t\xff'),
+ BasePatchData(address=318243, patch_data=b'\x00\x05\x04S\x05\x07S\x05\nS\x08\nS\x08\x07S\x08\x04S\x08\xe7\x19\x07\x07\xe3\x07\x08\xe3\x08\x07\xe3\x08\x08\xe3\xff'),
+ BasePatchData(address=318513, patch_data=b'\x00\x06\x08\x88\xff'),
+ BasePatchData(address=318667, patch_data=b'\x00\x07\x06T\x07\tT\t\x07T\xff'),
+ BasePatchData(address=319262, patch_data=b'\x00\t\t\t\xff'),
+ BasePatchData(address=319561, patch_data=b'\x00\x06\x07\x92\xff'),
+ BasePatchData(address=319934, patch_data=b'\x00\x07\x06T\x07\tT\t\x07T\x18\x17\xd1\x1c\x03\xc5\xff'),
+ BasePatchData(address=320599, patch_data=b'\x00\x05\x07\xbd\xff'),
+ BasePatchData(address=320954, patch_data=b'\x00\x05\x07\xcb\x05\x07\xcc\x05\x07\xcd\xff'),
+ BasePatchData(address=321108, patch_data=b'\x00\x05\t\xce\xff'),
+ BasePatchData(address=321671, patch_data=b'\x00\x05\x04S\x05\x07S\x05\nS\x08\nS\x08\x07S\x08\x04S\x08\xe7\x19\xff'),
+ BasePatchData(address=322049, patch_data=b'\x00\x05\x07\xa3\x05\x07\xa4\x05\x07\xa2\xff'),
+ BasePatchData(address=439998, patch_data=b'\x00'),
+ BasePatchData(address=440745, patch_data=b'\x00'),
+ BasePatchData(address=819304, patch_data=b'\x881\xfc8'),
+ BasePatchData(address=958593, patch_data=b'\\\x82\xb76'),
+ BasePatchData(address=961895, patch_data=b'"\x9c\x996'),
+ BasePatchData(address=972942, patch_data=b'"\xe7\x996\xea\xea\xea\xea\xea\xea\xea\xea'),
+ BasePatchData(address=973746, patch_data=b'\xa2\x07'),
+ BasePatchData(address=974570, patch_data=b'"z\xb76'),
+ BasePatchData(address=986033, patch_data=b'\x95\xc7'),
+ BasePatchData(address=988440, patch_data=b'"\x87\x996'),
+ BasePatchData(address=996017, patch_data=b'"\x1a\x976\xea'),
+ BasePatchData(address=999751, patch_data=b'"\x02\x9a6\xea\xea\xea\xea\xea'),
+ BasePatchData(address=1024348, patch_data=b'\xff\xff\xff\xff\xf0\xffa\x18\xff\xff'),
+ BasePatchData(address=1245184, patch_data=b'\t\x00\xe1>\x7f\x7f\x00\x00\x19\xff\xf0p\x04t\x07\x881\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\n#]r\xc6\xa4\xe5\xc3\xa0D\xa0\x0cO \xa4\xee^\x1cM\x11\xf2\xd3J\xa0\xb4B\x02=!\x00 \xe0\xb0\xf2\x1d\xce\xef\x15~\xcd-\xcc}\xb1xj2\xd2\x00\x0e\xb0\x9f\xee\xee\xbd\xd7RF$\xb4\x1d\x0f\x1f\x0b]\x1e.\x04\xb0\xf4\xe5/\x0e\xd8\xe9\xad\xee\xa4\x01-\xf5?G\xd3O\xd5\xb0O\xfe\n\xda\xdf\x01\xc2\x10\xa8l\x02\xf2\x96\xfe\x87\xd5\x10\xa49C\x17\x10\x15\xd1&\xee\xac\xf1\xc3\xe4\x92D\xef!h\xa4\x10\xa2\xcf)\x10\xec\x11\xf0\xac\xd1\xf7\x05\x8cr\xb3\xbeA\xa4\xe8\xd4\x00\x91!c\x0fs\xa4\x04\xcb-\xf9\xa1;|A\xb4/\xd5\x10\x0f\xf2P\xed\r\xb40\xbd-\x13\x022\xfe%\xb8\xb0\xed}\x1f\xe3\xf4\xc0O\xb4\x10\x00\x11\x10\xb2\x0e\xef\xa3\xa4a\xdcMg:t\xe6\xe3\xb8\xdf\xb4A\xc4\xecq\r\xe3\xbc\xd3\x0b \xd0\xf2 -\xf6\xbc\xf0\xef\xf3\x01\xfa?<=\xb4\x10\xf1\x06\x01\x0cU\x12\r\xc0-,\xbd\xbb\xdd\xf02A\xcc{\x1f\xff>\xf2\xe1"\xe0\xb42\xb6\xeeL+\xb6\xde\xfc\xbcP1\x91<5\x94\xfdN\xb4\xd2\xbd\xceB\x13\xef`#\xb4\xf2\xd1\xd9<\xa3\xf0qO\xb4\x07\xde<\xd4\xcd\xfc,C\xb0\xe6b%\x04!\xedR\xe1\xb4\xdb\x14\x02\xf5\xb0L#\xe1\xb4?\xee\xec\xa6\x1dq\xe2^\xb4\xf1K\xf4\x9d*\xff42\xb4=`\xa2\x0c\x12\xf9\xf3\xbe\xb0\xef\x17R5@\x01\xd1^\xb0\xcf\xba\xbe\xefS\xe5A4\xb4\xe01\x8d\x1a\x15\xd23\xf2\xbc+\x15\x90@\x84\x1c%\x1c\xbc3\xa2I\x04\xfe\x12\xbem\xbc\xd6K?\xc1@\xa2=\x15\xb0;\xed\xab\xde\xf3>\x05"\xb4.\x03)\xdf\xc2?\x13/\xa4\x16\x00<\xe1\xf9\xc9\x93g\xa8\xa5\xdcR\x80[O\x187\xb0\x89\xd0&P63/\xd4\xb4\xfb\xe2\xa3/%\x0b#\xd3\xb4<\xd6\xfc\xfe\xa1_\x03\x1c\xb4D\xc2]\xc4\xfb\xf1\xaeP\xb4\x06\x1b\'\xc0\\\xc6\x08\x0c\xb0\x88\xfe\xc7m\x05`q\xc3\xb0n\xbe\x98\x01\xc2N\xf3\xf0\xcc.\xd5\x0b1\xa2{\x03\xfb\xb4$\xd1Q\xc4\xd9 \xbel\xb45\xfd3\xe2\xe0\x04\xf9\x0f\xb4\xd0}\xf7\xfb2\xd4+\xe5\xb0=\xff\x99/\xd4=\x03\xee\xb0b\x17N\xe0\x98/\xc4\x1b\xb4S\xc2L\x07\xeb\xef\xceq\xb4\xc5\nS\xe2-\xe7\t\xd0\xb0\x882\xd3,%\x03O\xf5\xb4,\xa0\xdfU\x0c\x0f\xe5=\xb8>\x15)\xb2MW\x8c=\xbc\xf7\x1b\x00\xf5J\xeb\x13\x15\xb8\t\x12\xc3[\xd3\xf3L\xad\xb0\x9a\x93_\xf1\xc1R\xf0\xe0\xb0w\xeb\xc9\xb7P\x0e\xb2R\xbc\xf3\xe5X\xa1@&\xbb\xf4\xbc\xf3+\xd3!M\xabE\x11\xa4P\x8bBA\xac6s\x1c\xb8\x95a\xe1\xedO\xe1\x0e3\xb4B\x1e\x9a\x14\x03/\x01\xfe\xb0\xec\xc1Fu\xa9\xdd\xf4\x0e\xb0!\x11\xfc\xe1Gq\x9a\xed\xb05\xdd #\x0b\xf2\x15c\xac\x93w\xe8B\xb6\xea1\xf3\xbc-1\xbc\xe54\xd1\t\x14\xb4\xf0\xd4A\x0cP\xb8A\x16\xbc\xcf\xde0!\xd5\xff\xff\x86\xb4\x117\xef\xfc\xf0\x12\xe5\x12\xb0a\x99\xc0$\x111\x0e\xdb\xb0\xd23R\xec\xab$!.\xa4?\xcasD\x0f\x1e\x88\xd7\xacy\x8e\x13J\x04\x04(\xd7\xbc\xe2\xccc<\r\xd0P\x02\xbc\x0e\x1f\x0f\xa3B/\xcf\xb4\xb4\x1111\x00\r\x82\x07\x12\xb4\n\xef\x01\x12"\x10\x0b\x93\xb8\xf2\xb7\x8d3\xd3:\x17\x90\xb0"\x0e\xa9\x172\x0f\xcd\xf1\xb0#B\x10\xf2C\xc8\xd4S\xa8\xf0\x04^\xee1\r\x1c\xa0\xbcd\x0b\x1e\xd1O\x13\xef\xf0\xb0>\x8a\xb3U=\xba\xde\x13\xbc\xf0\xf0\x1a\x06"\xa4\xd9S\xbc\xe2L\xf0*#\xd2\xf4\x00\xa0[\xca\xa0De\x0e\xd0v\xb0\xfa\xb2c\xec\xd11\xf0\x12\xb0\x11#N\xa8\xc5T\xfe\xde\xb0\xff\xf143/\x99\xb2T\xbc\xfe\x02\x11/\xff\x00\x195\xb8\x01\xb5\x9do@\xff\x0e\x10\xbc\x1b\xf6z\xc2\r\x13\xf2,\xc4\xff\x1f!\xdf\xb7?\xcf0\xb0\xe0\xf7D\xdf\xf53\x08\x90\xbc\t\xc7:\r}0\xc5\xc3\xb0+\x8a\xf72\x1f\xcd\xc03\xb0!\x13\x18\x9e5\x02\x0e\xdc\xb8!9\x10\x02\x19EX\xb2\xb4\x1c\xe4\xc4\x1d\xcdC!\xba\xb4G-\xb0P\xf33,!\xb4\xd1\x0e\x8a\x06\x0b\xd3@\xce\xbc1\x02\xff\x02\xeb\xf6n\xa0\xb4\xe0\xdf\xf33/\xf2\r\x92\xb4"\xce\xfa\xee\'\x1d\x02\xf0\xbc\x1d\x07;\xc1l\xb3a\xdc\xbc\xd2=>\xc1b\xc9F\x18\xc4\xc1P\x0e\xf3\x10\x1e\xefb\xb4\xb8E\r\xd4A\xc1Q\xfc\xb8\x0bf\x1a\xc4\x1e\x1dS\xbf\xbc\x00/\xfb6K\xa5\x00\xc2\xbc\x17\x8e@\xdc\x7f\xe3\xf4\xdd\xbc\xb3_\xd39@\x02\xa6\x86\xb4d\x0e\xa11\xb4\x7f\xa1@\xb0<\xb0\xb8\xc0\xda\xb1\x1f\xfe\xb4M\xb3/9\xffw\xfcU\xb4\t\xc0O\x10\r\xb0\nV\xb4+\xb4-\x0f%\xe5,\x0e\xb0\x0e\xcdb\x00\xe0\xf3Fw\xb4\xff\xeb\xe4\xec\xb7M\xd1\x11\xb0\x9c\xfd~2\xeet_v\xb0Oe\xfc\x9d\xfd\xf1.\xd5\xb0J\xce\xdd\xcd<\xb8\x02\xf2\xb0AMB\xe13CUQ\xb010\xde\xdf\xee\xc9\x80>\xb4\x12\x1b\x95/\x11#\x04\x10\xb4\xf8T\x91^\xdc\xd4\xe2\xe2\xb4m\xd3\xdb\x1e^\xe6\x1d>\xbcN\xd3.\x1f\x0f\xf6\xdd}\xb0SF=\x1d\x9b\x9b\xfb\xf1\xb4\xde\x13\xdaf\x903\xff\x05\xb8\x96\xdeL\xf2\xf3\xa6L\xc5\xb42\xda/\x911\xd0o\xb4\xb41.\x10\xf4\xed\x1d1\xd3\xb4N\x0e\x1d\xf5\xea0\xee?\xb4\x0fj\xd2\xf1\xfdQ\xe2r\xb4\x0fm\x91>\xdd\x0f\xf3\x0e\xa03\x13/\xac,\x9b\xae\xc8\xbc2\xe0\x00\xe1Z\xe5\x00\x01\xa4\xe2\x14\x8c\x1e\xfe\x90/\xf1\xa0\x8c\xd7\t\xae\xbe\xdc\xbfe\xb4\x04\x01\xfe\xe1\x00\xdf\xf0@\xa4\xd3_\xff\xab\x05\xaaO\xf1\xbc\x1e_\xfd\x12\xf1\xfbA\x1f\xa4O\x0fx\xd4<\xed\xcfp\xa4\xacA=\xbc?\x11\xf2W\xa4 t\xed\xfc\xd2\x08\xa5\x01\xb0(\xf1D\x13\r\xfe\x0c\x1b\xb8D\xd2<\xb5\x04\xa4\xc0"\xa4\x13\xe5\x0c\x19a\x8c\xf0\xe3\xb0+\xbd\x0f\xff\xbb\xcd\xde\xdf\xac\x1ek[\xa3\xf4\xd6\x8bs\xa4\x03=?-\x02\xa0\xee\xe1\xa4\x1d\xd3VN\xddb\x1f\xd0\xa4\x14\x1f2\xbe\x0f?\xcb\xb4\x945\x88\xe7M\xbf\xa0^7\xa4$\xf4cK\xbe\x11\xfd\xae\xac\x13\xf0\x01\x0cMM\xe1\xb5\xa8\x00\xee&\xfd\xfd]n\xb3\xa4\xf2! \x1e\xbe!\xfe\x8d\x94\xf7#\x8e1\xfe\xf4\x90\xd1\xac0\xe2\xe5\x0f\xdd@\xf2\xb0\x94\xcd \xf0\x1f\xfbd\x1b\xb8\xa4\xf2\x0e\xfe4O\xff\x10P\xa4"\xf2\x01@\x1d\xc0\x1e\xf9\x94\x8f\xf5\xed\xe0\x1e\xd2N\x0e\x98\'\xc0\xe3\x17\x80\x85]=\xa4\xcf\xef\x01\x02\xdf\xe2A\xfd\x9c\xa7=.\xf5\xe4\xc2\x10\xf5\x98\xd2X>\x15\xd2\xbeA\xd0\x9c\x93P>\r-=P\xde\x98\xf2|\x1e\xf3m\n\xf4\x01\x94\x1c\x1d\xae\xef<\xeb\x170\xa4\xfc\xe0\xe0\x01\xee\xb3\x7f|\xb4\xf1#\xf3.\x1a\x7f\r-\xa4\xcc\xe2\xd0\x0f\xf0\x93\x05\x02\xa4\xd1\xfds\xf3-Q \x00\x94\xa1\x14\xed\xa2\xce\x1d\x0f[\xac \xb4\x00\x02\xc3\xe2\x0e>\xac\x00_\xf2\xef\x01\x11\xe0\xfe\xa8\xe2\x13\xc1\x1e<\\\xf1.\x94\xf7\xd0\xd6\x113cB\xe0\x9c\xd7\xee\xf1\xe0\x01\r-B\x98+\xde\x10"\xe0,\xf1/\x943#W5\xf2#?\xfe\x88\xa3\xb22\x0f\x13\xbe\x0e%\x8cJ\xff\xc6\x11\xe3\x021\xee\x8c\x1f\x1fm\xc3\x90\x02\xe1\x04\x8c\x1f\xfa\xc2\xd3?/\r\xf4|$%\'^\x83\x05o\xa8x\x0e\xa1\x10A\x01/\x8d\x03\x88B\xdb\xf0\xe1O\xf4\xf0\x1e|\\Z@=\xb0\x8f/\xf5\x84C\x1e\xb8\x9c\xbf\x02\xfe\xfe|5"Ec\xfd\xd6$\x0ex\x8e\x0b\xe2O!\x00\xee\xc3y\x15\x0e\xac\x0f\xe21\x13\xf0\x00\x00\x00\x08'),
+ BasePatchData(address=1343488, patch_data=b'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk'),
+ BasePatchData(address=1769728, patch_data=b'\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+ BasePatchData(address=1770352, patch_data=b'\x00\xd8\xe3\xd86'),
+ BasePatchData(address=1774837, patch_data=b'\x84\r\xaf\x00\x816\xd0\x12\xc0\x04\xd0\n"q\xba\r)\x03\x18i\x13\xa8\xb9\xf3\x81k\xdaZ\x8bK\xab\xc0\x04\xd0#"q\xba\r)\x03\xa8\xb9p\x83\x82C\x00\x00\xd9>y\xd9\xdc\xd8\xda\xe4\xe1\xdc\xd8\xdf\xe0\x0bB\xd3A\xd4\xd9\xe3\xd8\xc0\x0f\xf0\r\xc0\x11\xf0\t\xc0\x10\xf0\x05\xb9%\x95\x80\x1c\xaf\n\x04~\xa8\xaf\xc5\xf3~\xc9\x03\x90\x0c\xaf\n\x04~\xc9@\xb0\x04\x18i\x90\xa8\xb9 \x81\xabz\xfakH\x08\xe2 \xaf\xb0P\x7f\xf0$)\x01\xd0\n\xaf\xb0P\x7f)\x02\xd0\x0e\x80\x16"\xa5\x956\xa9\x00\x8f\xb0P\x7f\x80\n"\x17\x966\xa9\x00\x8f\xb0P\x7f(h\x8b\xa9\x00\x00[\\\xd5\x80\x00H\xad\x00CH\xad\x01CH\xad\x02CH\xad\x03CH\xad\x04CH\xad\x05CH\xad\x06CH\xa9\x80\x8d\x15!\xa9\x00\x8d\x16!\xa94\x8d\x17!\xa9\x01\x8d\x00C\xa9\x18\x8d\x01C\xa9\x9a\x8d\x02C\xa9\xc7\x8d\x03C\xa96\x8d\x04C\xa9\x00\x8d\x05C\xa9\x10\x8d\x06C\xa9\x01\x8d\x0bBh\x8d\x06Ch\x8d\x05Ch\x8d\x04Ch\x8d\x03Ch\x8d\x02Ch\x8d\x01Ch\x8d\x00ChkH\xad\x00CH\xad\x01CH\xad\x02CH\xad\x03CH\xad\x04CH\xad\x05CH\xad\x06CH\xa9\x80\x8d\x15!\xa9\x00\x8d\x16!\xa94\x8d\x17!\xa9\x01\x8d\x00C\xa9\x18\x8d\x01C\xa9\x9a\x8d\x02C\xa9\xd7\x8d\x03C\xa96\x8d\x04C\xa9\x00\x8d\x05C\xa9\x08\x8d\x06C\xa9\x01\x8d\x0bBh\x8d\x06Ch\x8d\x05Ch\x8d\x04Ch\x8d\x03Ch\x8d\x02Ch\x8d\x01Ch\x8d\x00ChH\xad\x00CH\xad\x01CH\xad\x02CH\xad\x03CH\xad\x04CH\xad\x05CH\xad\x06CH\xa9\x80\x8d\x15!\xa9\xa0\x8d\x16!\xa9:\x8d\x17!\xa9\x01\x8d\x00C\xa9\x18\x8d\x01C\xa9\x9a\x8d\x02C\xa9\xdf\x8d\x03C\xa96\x8d\x04C\xa9\xc0\x8d\x05C\xa9\x00\x8d\x06C\xa9\x01\x8d\x0bBh\x8d\x06Ch\x8d\x05Ch\x8d\x04Ch\x8d\x03Ch\x8d\x02Ch\x8d\x01Ch\x8d\x00Chk\xa9\x10\x85\xbc\xaf\x03\x816\xf0\x0b"q\xba\r)\x1f\x18i`\x85\xbc\xa9\x00\x8f\xb1P\x7f"=\x89\x00kH\xaf\x06\x816\xd0\x07h)\x03\x9d\xe0\rkhH)\x03\x9d\xe0\rhk\xaf\xf3\x0c~\xf0\x07\xa9\x05\x85\x11\x9c\xf3\x0c"\x00\x80\x07k"\x14\xc1\t\xa5\xa0\xa6\xa1\xc9\x07\xd0\x0f\xe0\x00\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\xdc\x00\xc9\xc8\xd0\x0b"N\xc4\t"\x14\xc1\t\x82s\x01\xc9)\xd0\x16"N\xc4\t"\x14\xc1\t\xa9\x07\x8d\x00\x0b\x9c(\x0b\xee\x08\x0b\x82Y\x01\xc93\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x9d\x01\xc9Z\xd0\x0b"N\xc4\t"\x14\xc1\t\x82;\x01\xc9\x90\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x7f\x01\xc9\xac\xd0\x1d"N\xc4\t"\x14\xc1\t\xaf\x01\x816\xf0\x0c\xeeh\x04\x9c\x8e\x06\x9c\x90\x06\xee\xf3\x0c\x82\x0b\x01\xc9\x06\xd0\x0f\xe0\x00\xd0\x0b"N\xc4\t"\x14\xc1\t\x82K\x01\xc9\xde\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x96\x00\xc9\xa4\xd0\x0b"N\xc4\t"\x14\xc1\t\x82-\x01\xc9\x1c\xd0\x0f\xe0\x00\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\xc7\x00\xc9l\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x0b\x01\xc9M\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x03\x00\x82L\x01\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd\x10\r\x18ih\x9d\x10\r\xbd\x00\r\x18ih\x9d\x00\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x08\x0b\x18ih\x9d\x08\x0b\xbd\x18\x0b\x18ih\x9d\x18\x0b\xe8\xe0\x08\xd0\xe0\x82\xf9\x00\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd \r\x18i\x00\x9d \r\xbd0\r\x18i\x01\x9d0\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x10\x0b\x18i\x01\x9d\x10\x0b\xbd \x0b\x18i\x00\x9d \x0b\xe8\xe0\x08\xd0\xe0\x82\xa6\x00\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd \r\x18i\x01\x9d \r\xbd0\r\x18i\x01\x9d0\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x10\x0b\x18i\x01\x9d\x10\x0b\xbd \x0b\x18i\x01\x9d \x0b\xe8\xe0\x08\xd0\xe0\x82S\x00\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd \r\x18i\x01\x9d \r\xbd0\r\x18i\x00\x9d0\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x10\x0b\x18i\x00\x9d\x10\x0b\xbd \x0b\x18i\x01\x9d \x0b\xe8\xe0\x08\xd0\xe0\x82\x00\x00k\xad\xba\x0c\xd0\x0b\xa9\x01\x8d\xba\x0c\xa9\x01\x8f\xb0P\x7f"\x7f\xd9\rk\xad\xba\x0c\xd0\x0b\xa9\x01\x8d\xba\x0c\xa9\x02\x8f\xb0P\x7f\xa9\x03\x9d\xc0\rk\xc5\x00\x90\x058\xe5\x00\x80\x02\xa9\x00kH\xa9x\x9d\x10\r\x9d\x00\r\xa5#\x9d0\r\xa5!\x9d \r\xa5\xa0\xc9\x07\xd0\n\xa5"\x9d\x10\r\xa5 \x9d\x00\rh"\xbd\x84\x06k\xda\xaf\x02\x816\xaa\xa5\x90\x18i\x04\x00\x85\x90\xa5\x92\x18i\x01\x00\x85\x92\xca\x10\xed\xfak\xbd\xe0\r\x1a)\x03\x9d\xe0\r\xbd\xa0\r\x1a\x9d\xa0\r\xc9 \x90\x03\x9e\xd0\rk\xaf\x05\x816\xd0\x05"\xfa\xc6\x1ek\xbd \x0e\xc9\xb8\xf0\x05"\x1a\xc7\x1ek\xa9\x83\x9d \x0e"\x18\xb8\r\xa9\xb8\x9d \x0e\xbd\xaa\x0c)\xfb\t\x80\x9d\xaa\x0c"\r\xc7\x1ek\xaf\x05\x816\xf0\x0c\xbd \x0e\xc9\xb8\xd0\x05\xa9\x83\x9d \x0e\xbd \x0e\xc9zk\xbd \x0e\xc9\xb8\xf0\r\xaf\x05\x816\xf0\x0c\xbd \x0e\xc9\xb8\xd0\x08\xa9\x83L\x82\x9a\xbd \x0e\xc2 \n\nk"\x9e\x9a6\xa9\x00\x8f\x11\xc0~kx"\x88\x88\x00kx"\x88\x88\x00k\xaf\xb1P\x7f\xf0\x01k\xa9\x01\x8f\xb1P\x7fx\x9c\x00B\x9c\x0cB\x9c6\x01\xa9\xff\x8d@!\xa9\x00\x85\x00\xa9\x80\x85\x01\xa9&\x85\x02"\x88\x88\x00\xa9\x81\x8d\x00BXk'),
+ BasePatchData(address=1783505, patch_data=b'\x8bK\xab\x08\xc20\xa0\x00\x00\xafY\xf3~)\xff\x00\n\xaa\xbd\t\xb7\xaa\xbf\xd1\x9a6\xda\xbb\x9f\x00\x90~\xfa\xbf\xd1\x9c6\xda\xbb\x9f\x80\x91~\xfa\xe8\xe8\xc8\xc8\xc0\x80\x01\x90\xe1(\xabk\x00\x00\x00\x00\x00\x04\x00\x08\x00\x0c\x8bK\xab\x08\xc20\xa0\x00\x03\xafZ\xf3~)\xff\x00\n\xaa\xbdK\xb7\xaa\xbf\xd1\xaa6\xda\xbb\x9f\x00\x90~\xfa\xbf\xd1\xac6\xda\xbb\x9f\xc0\x90~\xfa\xe8\xe8\xc8\xc8\xc0\xc0\x03\x90\xe1(\xabk\x00\x00\x00\x00\x00\x04\x00\x08\x85\x01\xa0\x00\x00\xa9\t\x85\x02k\xa9y"]\xf6\x1d0\x14\xa5"\x99\x10\r\xa5#\x990\r\xa5 \x99\x00\r\xa5!\x99 \rk\x9e\xba\x0c"\x18\xb8\rk\xa5\xa0\xc9\xac\xd0\x0e\xaf\x81\xde\t\xf0\x08\xaf\xcc\xf3~\\\x85\xa0\x1d\\\x90\xa0\x1d'),
+ BasePatchData(address=1787802, patch_data=b'\xcf=\xef^\xb7o\x9fp\x88x\xc7?g\x9fp\xaf\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\xfc\x03\xc7xc\xbd1\xde\xd8\xef\xf6\xe7\xf6g\xf6\xa7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x08\x00\x08\x00\xc0\x00\xe0\x00\x7f\x00\xbf\x80\xb8\x00\xb7\x07\xa8\x0f\x9f\x10\xff\x00\xff\x00\xff\x00\x7f\x00\x7f\x00x\x00p\x00`\x00\x00\x00\x00\x00\x1c\x00\xbe\x00\xe3\x00]\x1c\xa2\xbe\xff@\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xe3\x00A\x00\x00\x00\xdf\x10\xe0\x00~\x00=\x01;\x025\x065\x065\x06\xe0\x00\xff\x00\xff\x00\xfe\x00\xfc\x00\xf8\x00\xf8\x00\xf8\x00\xfc\x03\xce\xf1\xd7\xf8\xeb\x0c\xfc\xcf\x1e\xe7\xff\xfb?\xc0\x00\x00\x00\x00\x00\x00\x10\x00\xc0\x00\x00\x00\xf8\x00\x00\x00\xf7\x07\xef\x0f\xee\x0el\x0c\xb7\x87\xbb\x83\xdf\xc0\xbf\xe0\xf8\x00\xf0\x00\xf1\x00\xf3\x00x\x00|\x00?\x00\x1f\x00\x0f\x00\x0f\x00g`d`\t\x01\x9b\x83\xf5\x07\xe9\x0f\xff\x00\xff\x00\x9f\x00\x9f\x00\xfe\x00|\x00\xf8\x00\xf0\x00\xef\x0f\xf2\x03\xfd\x01\x8f\x00wp\xfb\xf8\xef\xec\xcf\xcc\xf0\x00\xfc\x00\xfe\x00\xff\x00\x8f\x00\x07\x00\x13\x003\x00\x07\x043030\x06\x00\xcd\x01\xf2\x03\xe4\x07\xef\x0f\xfb\x00\xcf\x00\xcf\x00\xff\x00\xfe\x00\xfc\x00\xf8\x00\xf0\x00\xff\x00\xff\x7f\xffo\xefV\xfeY\xefM\xe5G\xf7W\x00\x00\x7f\x00o\x00F\x00@\x00P\x00X\x00H\x00\xff\x00\xe0\xdf\xdc\xc1[\xdc\xa5\xee\x02\x07Ss\x8b\xfb\x00\x00\x00\x00\x00\x00 \x00\x10\x00\xf8\x00\x8c\x00\x04\x00\xd4\xf7\xea\xef\x15\x1e;\xfc\xf6\xf9|\x83\xc6;\x86\xfb\x08\x00\x10\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe7W\xe5W\xed_\xed_\xfdO\xf5O\xf8G\xe4[H\x00H\x00@\x00@\x00`\x00`\x00`\x00@\x00\xd2\xd6\xf3\xf7\xe5\xf7\xea\xfb\x80\xfcE\xc7\xba\x83\xed\xa9)\x00\x08\x00\x08\x00\x04\x00\x03\x008\x00D\x00\x12\x00\xabn\xaam\xabm\xabm\xabm\xbbm\x9bU\xabm\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00(\x00\x10\x002\xeel\xdc\xda\xfb\x90\xb7\x117\x146\x132)9\x01\x00\x03\x00\x04\x00H\x00\xc8\x00\xc9\x00\xcd\x00\xc6\x00\x00\x0f\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x0c\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Of\xbf\xe6\x19\xcag\xf4S4\xed*\xd7Z\xab\xb6\x98\x00\x18\x004\x00\x08\x00\xc8\x00\xd0\x00\xa0\x00@\x00C\xc0\x89H\x9c\\\x9eR\x8eR\x9cJ\xbcj\xae\\?\x007\x00#\x00!\x00!\x00!\x00\x01\x00\x01\x00\x7f\x80\x80\xff\x03\x0269\x7fg?!\x99\x1e\xc0\x00\x00\x00\x00\x00\xfc\x00\xc0\x00\x80\x00\xc0\x00\xe0\x00\xff\x00\xf0o\xfao\xfao\xf0n\x90o\xb0/\xf8o\xf8o\x00\x00\n\x00\n\x00\x01\x00\r\x00B\x00\x0b\x00\t\x008\xc7\x00\x7f\x03\xff\x84\xfc\x06\xfe!\xff\xa0\xffF{\x00\x00\x83\x00\x07\x00\xcf\x00\x8f\x00G\x00\x83\x00\xe6\x00\xff\x00\xf3\xfb\xf7\xff\xff\x00c\xff\x00\xff`\xff\x00\xef\x00\x00\x04\x00\x00\x00\x00\x00k\x00\x08\x00f\x00\x1b\x00\x12\xbe\x01\xff\x86\x7f\x80\x7f\x80\x7f\x19\xfe)\xef$\xe7[\x00\x15\x00\x01\x00\x00\x00\x18\x00=\x00\x7f\x00~\x00\xc8\xe0[\xf3\xd4w\xe4w\x94W\xa8kr\xb3\xde8\x1f\x00\x0c\x00\x08\x00\x08\x00(\x00\x14\x00\x0c\x00\x07\x00\x03\x0f\xe2\xefg\xeeg\xee\xed\xeeM\xce\x96\x9d+<\xf0\x00\x10\x00\x10\x00\x10\x00\x10\x000\x00`\x00\xc0\x00\xbd\xc2\xf3\xfc\x16\xf7k{\xc5\x0598C~@~\x00\x00\x00\x00\x08\x00\x84\x00\xfa\x00\xc7\x00\x81\x00\x81\x00J~||yy\x02\x03\r\xff>\xff\xf3\xfc\xbd\xc2\x81\x00\x83\x00\x86\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\xefX\xecZ\xecZ\xeb]\xef]\xf7N\xfbW\xfdZ@\x00A\x00A\x00@\x00@\x00`\x00@\x00@\x00\xfe<_=\xbe\x82\xcfLP\xb0\xbf\xc0\xfbgY\xb7\x01\x00\x80\x00A\x000\x00\x0f\x00\x00\x00\x00\x00\x00\x00.\xd9:\xd5\xb0\xcf\xe7\x1fL\xbcZ\xb8d\xb0\xf19\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x07\x00\x0f\x00\x06\x00\xa7\xe5\xbd\xfa[~c>\xb5\x86KL\xfe\x81\x8ds\x18\x00\x00\x00\x80\x00\xc0\x00x\x00\xb0\x00\x00\x00\x00\x00(%QD\xa0\x85H\r\x90\x1d`}\x80\xfd\x00\xfd\x1b\x003\x00b\x00\xc2\x00\x82\x00\x02\x00\x06\x00\x06\x007\xce\xab\xda\xfb\x9a\xd76\xabnS\xde\xa7\xbe\x076\x00\x00\x04\x00\x04\x00\x08\x00\x10\x00 \x00@\x00\xc8\x00\xdf0\xc88\xe7_\xf0o\xffp\xff\x7f\xbf?\xff\x00\x00\x00\x07\x00\x00\x00 \x000\x00?\x00@\x00\x00\x00\xff\x00\xfd\xde\x9f\xee\xcfv\xaf6\xaf6\xaf6\x00\x00\x02\x00\x1c\x00\x0c\x00\x04\x00D\x00D\x00D\x00\x07\x03\x03\x01\x00\x00\x01\x01\x02\x02\x00\x06\x00\xf6\x06\x16\x04\x00\x02\x00\x01\x00\x02\x00\x84\x00\xf8\x00\x08\x00\x08\x00~\x81\xe7~\x91~\xb1~\x85~\x83>\xc3Z~\x81\x00\x00~\x00~\x00~\x00~\x00~\x00~\x00\x00\x00\x10\x908\xa8\x1c\x94\x0e\xca\x07\xe5\x03\xf2\x01\xff\x00\x00\x00\x00\x10\x00\x08\x00\x04\x00\x02\x00\x81\x00\xc0\x00\xff\x00\\D.#\x17\x11\x0b\x08\x05\x06\x82\x8f\xc0\x7f\x00\x00\x18\x00\x0c\x00\x06\x00\x03\x00\x01\x00\x01\x00\x83\x00\xff\x00\x00\xff\xc0\x00\x17\xf9\x0b\x08\x05\x04\x02\x02\x01\x01\x00\x00\xf0\x00\xff\x00\x06\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\x03\x00\x00\xff\x80\x80\xc0@\xe0 p\x10\xb8\x88\x0f\x00\xff\x00\x00\x00\x00\x00\x80\x00\xc0\x00`\x000\x00\x00\x00\x0e\xee\x03s\x00\xbc\x80\xdf@\xef\xa0\xf7\xd0\xfb\xfe\x001\x00\x1c\x00\x0f\x00\x07\x00\x03\x00\x01\x00\x00\x00\xe8\xfd\xf4\xfe\xfa\xff\xfd\x7f~??\x1f\x1f\x0f\x0f\x07\x00\x00\x00\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x08\x00\xef]\xf7M\xfae\xffp\xff|\xff\x7f\x80\x7f\xff\x00@\x00`\x00p\x00x\x00\x7f\x00\x7f\x00\x7f\x00\x00\x00\xea\xd7;\'\xd9\xd7z\xf7\xfb\x07\xf8\xf6\x04\xfb\xff\x00\x00\x00\xc0\x00 \x00\x00\x00\x00\x00\xf1\x00\xf8\x00\x00\x00\xff\x00\x90p\xafo\xdb\\\xb4;\xa97\xf3\x0f\xe7\x1c\x00\x00\x0f\x00\x1f\x008\x00p\x00`\x00\x00\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\x00\x18\xe7/\xdf\xd8\xb8\xf32\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x07\x00\x0c\x00\x00\xfd\x00\xfd\x00\xfd\x00}\x00\x1d\x00\r\x00\x01\x00\x00\x06\x00\x06\x00\x8f\x00\xff\x00\x7f\x00\x1f\x00\x0f\x00\x07\x00\xff\x00\x05\x06\xfb\xf8\xbfx]\xb2\xab\xd6[f\xf7\x0e\x00\x00\xf8\x00\xfc\x008\x00\x10\x00\x00\x00\x80\x00\x00\x00P\xdf\x04\xdf\xaf\xff\xc0\xff\x85\xe5\x00\xc0\x02\x82\x00\x00 \x00 \x00\x00\x00\x00\x00\x02\x00\x02\x00\x00\x00\x00\x00(\xef\x12\xff\x93\xff\x07\xffS\xdf!\'\x00\x03\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\xf8g\xf0o\xf3h\xf3d\x93d\xb3$\xf3d\xf9n\x00\x00\x00\x00\x04\x00\x08\x00\x08\x00H\x00\x08\x00\x00\x00x\x87\x87\xffL\xff\xbb|\x87H\x97X\xdf\x10\xe78\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x03\x00\x07\x00\x00\x00\x00\x00\x1f\x1fbcD\xc68\xfd\x00\xf1\x00\x8d\x00\x00\x1f\x00`\x00\x80\x00\x01\x00\x83\x00\xcf\x00\xf3\x00\x00\x00{;\xfd\xdd\xfe.\xdf\xf7\xef\xfbw};>\x7f\x00\xc4\x00"\x00\xd1\x00\x08\x00\x04\x00\x02\x00\x01\x00\x00\x00\xff\xfd\x01\x00\x00\x00\x00\x00\x80\x80\xc0\xc0\xe0\xe0\xff\x00\x02\x00\x01\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x00\x00\xf4\xfe\xfa\xff\xfd\x7f~??\x1f\x1f\x0f\x0f\x07\xff\x00\x00\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x08\x00\x00\x03\x00\x03\x00\x03\x00\x03\x00\x06\x01\x01\x06\x06\x08\x08\x06\x00\x06\x00\x07\x00\x07\x00\x0f\x00\x0e\x00\x08\x00\x10\x00\x06\xf6\x08\xe8\x10\x90`a\x80\x82\x00\x05\x00\n\x00\x14\x08\x00p\x00\xe8\x00\x88\x00\x05\x00\x02\x00\x04\x00\x08\x00\x00-\x00X\x01\xb9\x00`\x00\xd0\x00\xa0\x00@\x00\xc0\x16\x00/\x00\\\x00\xac\x00H\x00\x98\x00(\x000\x00 \xe0\x10\xf0\x08\xf8\x04|\x0f\x1f\x00`\x00o`h\x00\x00\x00\x00\x80\x00\x80\x00`\x00\x1f\x00\x10\x00\x10\x00\x1d\x1a\x1a\x1d\x1d\x1e8?\xe0\xff\x00\x00\x00\xff\xb8\x88\x05\x00\x02\x00\x01\x00\x00\x00\x00\x00\xff\x00\x00\x000\x00\xf0p\xf88|\x9c\xbeN_\xa7\x00\x00\x00\xff\x00\x00\x88\x00\xc4\x00b\x00\xb1\x00X\x00\xff\x00\x00\x00\x00\x00\x00\x80\x00\x80\x00\x80\xc0@\xc0\xe0`r0\xb7\x17\xd7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00@\x00 \x00\x01\x01\x00\x01\x00\x01\x03\x02\x03\x07\x06N\x0c\xed\xe8\xeb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\xf0i\xf3o\x99g\xacc\xdf8\xee\x1dw\x8e3\xce\x06\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x18\xef\x18\xff\x80\xff\xc0\xff\x00p\xef0\xef\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x02\x00\x02\x00\x03\x00\x00\x06\x01\x01\x0e\x0e\x100 \xe0F\xc7x\xfe!\xf9\x0f\x00\x1e\x000\x00\xc0\x00\x00\x00\x00\x00\x01\x00\x06\x00p}\x90\x99\x04\x15\x0cm\x1c\x9dhm\x80\x89\x00\x15\x82\x00\x06\x00\n\x00\x12\x00b\x00\x82\x00\x06\x00\n\x00\x1d\x1f\x0e\x0f\x07\x07\x03\x03\x01\x01\x00\x00\x80\x80@\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0p\xf88|\x9c\xbe\xce\xdf\xe7\xef\xf3wy;<\x88\x00\xc4\x00b\x001\x00\x18\x00\x0c\x00\x06\x00\x03\x00\x07\x03\x03\x01\x01\x00\x00\x00\x00\x00\x80\x80\xc0\xc0\xe0\xe0\x04\x00\x02\x00\x01\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x10\x10 ` `\x13s\x0c\x7f\x00~\x00}\x00z`\x00\x80\x00\x80\x00\x80\x00\x80\x00\x81\x00\xc2\x00\xcc\x00\x00)\x00S\x80\xa3\x04\xa69}\x00\xf9\x01\xc9\x11\x11\x10\x00 \x00@\x00A\x00\x82\x00\x06\x00\x04\x00\x08\x00\x00\x80\x00\x00\x00\x00\x01\x01\x81\x81\x02\x03<\xbf\x00\x7fP\x00\x90\x00\xa0\x00\xa0\x00@\x00@\x00@\x00\x80\x00\x80\xe8\x80\xe8\x84\xec\x02\xee\x02\xee\x02\xee\x01\xef\x01\xef\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x000\x00p\x00\xdc\xc4\xae\xa2\x17\x11\x0b\x08\x05\x042\x129)\x1c\x14\x18\x00\x0c\x00\x06\x00\x03\x00\x01\x00 \x00\x10\x00\x08\x00\x00\x00\x00\x00\x00\x00\x80\x80\xc0@\xe0 p\x10\xb8\x88\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\xc0\x00`\x000\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x01\x06\x06\x18\x18\x08\x08\x02\x00\x04\x00\x04\x00\t\x00\x0e\x00\x18\x00 \x00\x10\x00\x98\x98\x00\x00\x18\x18xx\xb0\xb000ppdl\x06\x00\x1c\x00d\x00\x84\x00\x08\x00\x08\x00\x08\x00\x10\x00\x00\x13\x00\x12\x00\x18\x01\x19\x03\x1a\x03\x19&2Dt\x0f\x00\x0f\x00\x07\x00\x06\x00\x05\x00\x06\x00\x0c\x00\x08\x00XI\xb0\x91\xe0\xa1\xc0A\x80\x83\x04\x04\n\x08\x14\x12\x90\x00 \x00B\x00\x84\x00\x08\x00\x13\x00\x07\x00\r\x00\x00\xc0\x00\xb0\x04\xa0\x08@ ` \xe0\x18\xf8\x07\xff?\x00@\x00D\x00\x88\x00\x80\x00\x00\x00\x80\x00\xc0\x00\x00\x03\x00\r \x05\x10\x02\x04\x06\x04\x07\x18\x1f\xe0\xff\xfc\x00\x02\x00"\x00\x11\x00\x01\x00\x00\x00\x01\x00\x03\x00\x00\xff@\xc0\x1f\x9f?\xbf?\xbf?\xbf?\xbf?\xbf\x00\x00?\x00`\x00@\x00@\x00@\x00@\x00@\x00\x0c\x0c\x07\x07\x07\x07\x03\x03\x01\x01\x01\x01\x00\x00\x00\x00\x10\x00\x08\x00\x08\x00\x04\x00\x02\x00\x02\x00\x01\x00\x01\x00co\xe0\xef\xc0\xdf\xc0\xdf\xc0\xdf\xc0\xdf\x80\xbe\x80\xbe\x10\x00\x10\x00 \x00 \x00 \x00!\x00C\x00O\x00\x000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00h\x03S\x0c\x0f\x00\x0f\x00\x03\x00\x00\x00\x00\xf8\x00\xf0\x00\xe0\x00`\x00\x10\x00\x0f\x00\x03\x00\x03\x00!!\xc3\xe3\x02\xc2\x06\x86\x18\x98dd\x9c\x9cll\x10\x00\x10\x00!\x00A\x00\xc7\x00\x9a\x00b\x00\x82\x00\x00\x03\x1c<\x03#\x00\x80\x00\x91\x00O\x00\'\x00\'\xfc\x00\xc3\x00\xc0\x00`\x00`\x000\x00\x1f\x00\x1f\x00\x00\xef\x00\x0f\x80\xef\x10\xd3\x10\xd1"\xa0&\xa3LE\xf8\x00\xf8\x00<\x00n\x00c\x00\xc3\x00\xc4\x00\x88\x00\x8e\x8a\x87\x85C\xc2A\xc10\xf0\x0c\xfc\x03\x7f\x00\x9f\x04\x00\x02\x00\x01\x00\x00\x00\x00\x00\x80\x00\xe0\x00x\x00]E)!\x92\x93\xceO\xec\xafp_\xc0\xfe\x00\xf9\x18\x00\x08\x00\x00\x00\x80\x00@\x00!\x00\x07\x00\x1e\x00\x03\xff\x1c\xff7\xf8N\xf1\x9c\xe38\xc7`\x9f\x18\xff\x03\x00\x1f\x00?\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xb8\xff\x1c\xff\x02\xff\x17\xef\x10\xefx\x87\xf0\x0f\xe0\x1f\xf8\x00\xfc\x00\xfe\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\xff\x00\xff\xaa\xff\xff\xff\xff\xff\x1f\xff?\xff\x1f\xff\x00\x00\x00\x00\x00\x00\xf0\x00\xfc\x00\xfe\x00\xff\x00\xff\x00\x00\xff\x00\xff\xaf\xff\xff\xff\xf0\xff\xec\xf3\x9d\xe3\x1a\xe7\x00\x00\x00\x00\x07\x00\x0f\x00\x1f\x00\x7f\x00\xff\x00\xff\x00\x00\xff\x00\xff\xaa\xff\xff\xff\xff\xff\x1f\xff\xff\xff?\xff\x00\x00\x00\x00\x80\x00\xf0\x00\xf8\x00\xfc\x00\xfe\x00\xff\x00\r\xfe\x01\xfe\x83|\xc78\xc78\xcf0\xde!\xdf \xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xc1?\x8cs\x8cs\x98g8\xc7q\x8e\xf3\x0c\xf7\x08\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xe7\xff\x0c\xff\x10\xff\x06\xf9\x9f`\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x04\xffr\xfd\xfe\xf9\x1f\xfc#\xde\xa0_\xf2\r\xf7\x08\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x1d\xffx\xff\xde9\xef\x19\xff\x08\xf7\x08w\x88w\x88\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\xff\xff\xff\xfe\xff\xbf\xff\x0e\xff\x8d~\xcb<\xf7\x18\xe0\x00\xf0\x00\xf8\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\xff\xfb\xff\xe9\xff\x98\xff(\xf7\x99g=\xc3~\x81>\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\xff\xff\xff\xf5\xf5p\xf0\xf8\xf8\x9c\xfc\xd2.\xfe\x01\x00\x00\x80\x00\xea\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\xff\xaa\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xffUU\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x03\x00\x06\x02\x0c\x04\x19\t3\x12&\x04\x00\x00\x00\x00\x00\x00\x01\x00\x03\x00\x06\x00\x0c\x00\x18\x00\x00\x00\x00\x00\xc0\x00`@0 \x98\x90\xccHd \x00\x00\x00\x00\x00\x00\x80\x00\xc0\x00`\x000\x00\x18\x00<\x00$\x00$\x00$\x00$\x00$\x00$\x00<\x00\x00\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x00\x00\xe7$\xc3B\x81\x81\x00\x00\x00\x00\x81\x81\xc3B\xe7$\x18\x00<\x00~\x00\xff\x00\xff\x00~\x00<\x00\x18\x00\x00\x00\x01\x00\x06\x00\t\x01\x0f\x07\x0f\x07\x1f\x0f\x1e\x0f\x00\x00\x01\x00\x07\x00\x0e\x00\x08\x00\x08\x00\x10\x00\x10\x00\xd8\x1f\x0c\x0f\xea\xef\xf9\xff\xfb\xff\xfe\xff\xfd\xff\xfe\xff\xf8\x00\xfc\x00\x1a\x00\t\x00\x0b\x00\x0e\x00\r\x00\x0e\x007\xce\x16\xefP\xef]\xe2\xdf\xe0\xcb\xf6\x8d\xf2\x87\xf8?\x00?\x00\x7f\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x07\xf3\x0f\xf0\x0f\xfe\x01\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x9a~\x95|\xaax\xcb9\xd5?\xed\x1f\xb5O\x15\xef\xf9\x00\xf2\x00\xe5\x00\xe4\x00\xf0\x00\xfc\x00\xfc\x00\xfc\x00\n\x07\n\x07\x05\x03\x04\x03\x04\x03\x02\x01\x01\x00\x00\x00\x0f\x00\x0f\x00\x07\x00\x07\x00\x07\x00\x03\x00\x01\x00\x00\x00\xff\x00\xff\x7f\xffo\xefV\xfeY\xefM\xe5G\xf7W\x00\x00\x7f\x00o\x00F\x00@\x00P\x00X\x00H\x00\xff\x00\xe0\xdf\xdc
\x1f<\x1f \x1f\x10\x00\x10\x00\x12\x002\x006\x00=\x003\x00?\x00?\xff?\xff\xc4\xff\x86\xff\x03\xff\x03\xfe\x07\xf8\x0f\xf1\x17\x00\x1b\x00=\x00~\x00\xff\x00\xff\x00\xff\x00\xff\x00\x87\xf8\x8f\xf0?\xcc\x7f\xbe\xcf\x7f\xc7\xff\xe0\xff\xf0\xff\xff\x00\xff\x00\xff\x00\xdf\x00\x8f\x00\x87\x00\xc3\x00\xe7\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xfc\x83p\x8f8\xdf\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xfc\x00\xf0\x00\xf8\x00U\xefU\xefU\xef\x95o\x15\xef\x0c\xf7\x04\xfb\x04\xfb\xfc\x00\xfc\x00\xfc\x00\xfc\x00\x1c\x00\x0c\x00\x04\x00\x0c\x00\x80\x7f\xb2\x7f\xa1~\x83|\x86y\x8cs\x8cs\x98g\xf0\x00\xfa\x00\xfd\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xe7W\xe5W\xed_\xed_\xfdO\xf5O\xf8G\xe4[H\x00H\x00@\x00@\x00`\x00`\x00`\x00@\x00\xd2\xd6\xf3\xf7\xe5\xf7\xea\xfb\x80\xfcE\xc7\xba\x83\xed\xa9)\x00\x08\x00\x08\x00\x04\x00\x03\x008\x00D\x00\x12\x00\xae\xe4\xe3j\xe3i\xd1D\xa0\x02\xdf\x00\xaf\x1f\xa3c\x13\x00\x15\x00\x16\x00;\x00}\x00`\x00@\x00\x1c\x00\xba\xc6E|\xbb9\xd6\x92\xedD\xffDUEEE\x01\x00\x83\x00\xc6\x00m\x00\xbb\x00\xba\x00\xba\x00\xba\x00\xd3\xc3\xd9\xc1\xb5\x81c\x00\xc3\x00\x83\x01\x87\x03\xff\x04<\x00>\x00~\x00\xfe\x00\xfe\x00\xfe\x00\xfc\x00\xf8\x00\xc4f\xa2\xb3p9\xa8-PE\xec%\xf4\xd5\xb8\xad\x99\x00L\x00\xc6\x00\xd2\x00\xba\x00\x1a\x00\n\x00B\x00\xbc\xfd|}xy\xf8y`a;\x01\xbf\xe1\x8e\xe2\x02\x00\x82\x00\x86\x00\x86\x00\x9e\x00\xfe\x00\x1e\x00\x1d\x00\x00\x00\x00\x00\x00\x00\x01\x00\x03\x01\x06\x03\x0c\x07\x1e\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\x0c\x00\x1e\x00\x00\x00\x07\x00y\x00\xf2r\xaf\xef\x17\xff$\xff\x01\xff\x00\x00\x07\x00~\x00\x8d\x00\x10\x00\x00\x00\x00\x00\x00\x00\x01\x00\xc7\x01\xfb\x07u\x0f\xa9\x97\xc9\xd7j\xf6\x8a\xf6\x01\x00\xc6\x00<\x00\x9c\x00\\\x00<\x00\x1d\x00}\x00 \x1f \x1f1\x0e\x12\r\x13\r\x17\x0b\x17\x0b\x15\x0b?\x00?\x00?\x00\x1e\x00\x1f\x00\x1f\x00\x1f\x00\x1d\x00?\xc1w\x9b\xf7{\xcb\xf7\x0b\xf7\x8d\xf3\x86\xf9\x87\xf8\xff\x00\xe7\x00\x87\x00\x0f\x00\x0f\x00\xbf\x00\xff\x00\xff\x00\xe0\xff\xe0\xff\xc0\xff\xc9\xfe\x82\xfd\x84\xfb\x08\xf7\xd1/\xff\x00\xff\x00\xff\x00\xff\x00\xf7\x00\xfd\x00\xfd\x00\xfd\x00<\xdf~\xbf|\xbf\xf8?x\xbf9\xdf\xd0\xef\xe8\xf7\xfc\x00\xfe\x00\xfd\x00\xfb\x00\xfb\x00\xff\x00\xff\x00\xff\x00\x06\xfb\x06\xfb\x07\xfb\x8b\xf7\x8b\xf7\x13\xef\x11\xef!\xdf\x1e\x00\xfe\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00p\x8f\xe8\x17\x84{b\xfdy\xfe=\xfe\x1c\xff\x10\xff\x7f\x00\xff\x00\xbf\x00\x7f\x00\x7f\x00?\x00_\x00\xff\x00\xefX\xecZ\xecZ\xeb]\xef]\xf7N\xfbW\xfdZ@\x00A\x00A\x00@\x00@\x00`\x00@\x00@\x00\xfe<_=\xbe\x82\xcfLP\xb0\xbf\xc0\xfbgY\xb7\x01\x00\x80\x00A\x000\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x0c%)#+&\x86\x0c\xe6\x0c6\x0c.4^t\xd2\x00\xd4\x00\xd1\x00\xf3\x00\xf3\x00\xf3\x00\xc3\x00\x83\x00\xc6\x83\xbb\x01E8\xba|\xfeD\xfeD\xfeD}\xa3|\x00\xc6\x00\x83\x00\x01\x00\x01\x00\x01\x00\x19\x00\x18\x00n\xfagjwvwrwr;9\x1d\x1c\x0e\x0e\x05\x00\x95\x00\x89\x00\x8d\x00\x8d\x00\xc6\x00\xe3\x00\xf1\x00E|T\xfc\x8a\xee\xc5g\xc2s\xc1i\xc8l\xc8n\x82\x00\x03\x00\x11\x00\x98\x00\x8c\x00\x96\x00\x93\x00\x91\x00\x00\x00\x03\x00\x07\x03\x0c\x07\x0c\x07\x1c\x0f\x1f\x0c<\x03\x00\x00\x03\x00\x04\x00\x08\x00\x08\x00\x18\x00\x1f\x00<\x00~\x1f\xff?\xdf?o\x9fo\x9f3\xcf\x9cc_\xa4~\x00\xff\x00\xff\x00\x7f\x00\x7f\x00\x7f\x00\xff\x00\x7f\x00\x00\xff\x80\xff\xc0\xff\xd4\xff\xc8\xff\x80\xff\x81\xfe\x0f\xf1\x01\x00\x87\x00\xdf\x00\xff\x00\xfb\x00\xfb\x00\xff\x00\xff\x00~\x81\xe7~\x91~\xb1~\x85~\x93~\xc3~~\x81\x00\x00~\x00~\x00~\x00~\x00~\x00~\x00\x00\x00\x0f\x07\x0f\x07\x1e\x0f\x1e\x07<\x03?\x00\x15\x0e\x12\x0f\x0f\x00\x0f\x00\x1f\x00\x1f\x00?\x00?\x00\x17\x00\x1b\x00\x07\xf8\x0f\xf0\x1f\xe08\xc7\xf3\x1f\xf9?\x9a\x7f\x88\x7f\xff\x00\xff\x00\xff\x00\xff\x00\xf3\x00\xf9\x00\xd8\x00\xe8\x00\xf3\x0f\xf7\x0f\xa5_-\xdf.\xdf\xee\xdf\xec\xdfh\x9f\xfd\x00\xfd\x00\xfd\x00\xfd\x00\xfe\x00\xfe\x00\xfd\x00{\x00\xf7\xf8\xf8\xff\xff\xff\xde\xff\xb9\xff\x02\xfb\x0c\xff2\xff\xff\x00\xff\x00\xff\x00\xde\x00\x99\x00\x07\x00\xfd\x00\xff\x00\xe1\x1f`\x9f"\xdf#\xdf\x13\xef\x11\xef\x10\xef\x18\xe7\xff\x00\xff\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xa1\xff\xc1\xff\x10\xff3\xfc\xff\x01\xfa\x1d\xed>H\xbf\xbf\x00\xff\x00\xff\x00\xff\x00\xff\x00\xef\x00\xcf\x00\xcf\x00\xef]\xf7M\xfae\xffp\xff|\xff\x7f\x80\x7f\xff\x00@\x00`\x00p\x00x\x00\x7f\x00\x7f\x00\x7f\x00\x00\x00\xea\xd7;\'\xd9\xd7z\xf7\xfb\x07\xf8\xf6\x04\xfb\xff\x00\x00\x00\xc0\x00 \x00\x00\x00\x00\x00\xf1\x00\xf8\x00\x00\x00\xff\x007\xedK\xd9\x91\xb3 d@I\x01\x13\x02&\x00\x00\x00\x00$\x00L\x00\x9b\x00\xb6\x00\xec\x00\xd9\x00\xff\x00\xfeW\xfeWV\xff\x02\xab\x80\xa9)m|\xc6\x00\x00\x00\x00\x00\x00\x00\x00T\x00V\x00\x92\x009\x00\n\xf6\x08\xf4\x14\xec*\xdaN\xbe\xc2>\xd3?\xca>\xfd\x00\xff\x00\xfb\x00\xf5\x00\xe1\x00\xe1\x00\xf0\x00\xf9\x00\xfc\x03\xaea\xafdA\xca\x89\x9a\x91\xb2!\xe6\xa1\xee\x00\x00\x10\x00\x10\x004\x00d\x00L\x00\x18\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbcCt\x0b:\x05\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x7f\x00?\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x18\xff\t\xfe\x02\xfd\xe7\x19\x1e\x01\x01\x00\x00\x00\xff\x00\xff\x00\xff\x00\xfe\x00\xff\x00\x1f\x00\x01\x00\x00\x00P\xbf\xe0_\xf1\xee~\xf0\xc2\xfc\x02\xfc\xfc\x00\x00\x00\xff\x00\xbf\x00\x7f\x00~\x00\xfe\x00\xfe\x00\xfc\x00\x00\x00@\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009\xc6\x7f\x81\xf9\x079\xf7\xe4\xfb$\xfb\x06\xf9\xf9\x00\xff\x00\xff\x00\xff\x00?\x00\xff\x00\xff\x00\xff\x00\xf9\x00'),
+ BasePatchData(address=46567, patch_data=b'6'),
+ BasePatchData(address=160226, patch_data=b"u\x83\x83\x83\x91\x83\x9f\x83\xad\x83\xbb\x83\xc9\x83\xd7\x83\xe5\x83\xf3\x83\x01\x84\x0f\x84\x1d\x84+\x849\x84G\x84U\x84c\x84q\x84\x7f\x84\x8d\x84\x9b\x84\xa9\x84\xb7\x84\xc5\x84\xd3\x84\xe1\x84\xef\x84\xfd\x84\x0b\x85\x19\x85'\x855\x85C\x85Q\x85_\x85m\x85{\x85\x89\x85\x97\x85\xa5\x85\xb3\x85\xc1\x85\xcf\x85\xdd\x85\xeb\x85\xf9\x85\x07\x86\x15\x86#\x861\x86?\x86M\x86[\x86i\x86w\x86\x85\x86\x93\x86\xa1\x86\xaf\x86\xbd\x86\xcb\x86\xd9\x86\xe7\x86\xf5\x86\x03\x87\x11\x87\x1f\x87-\x87;\x87I\x87W\x87e\x87s\x87\x81\x87\x8f\x87\x9d\x87\xab\x87\xb9\x87\xc7\x87\xd5\x87\xe3\x87\xf1\x87\xff\x87\r\x88\x1b\x88)\x887\x88E\x88S\x88a\x88o\x88}\x88\x8b\x88\x99\x88\xa7\x88\xb5\x88\xc3\x88\xd1\x88\xdf\x88\xed\x88\xfb\x88\t\x89\x17\x89%\x893\x89A\x89O\x89]\x89k\x89y\x89\x87\x89\x95\x89\xa3\x89\xb1\x89\xbf\x89\xcd\x89\xdb\x89\xe9\x89\xf7\x89\x05\x8a\x13\x8a!\x8a/\x8a=\x8aK\x8aY\x8ag\x8au\x8a\x83\x8a\x91\x8a\x9f\x8a\xad\x8a\xbb\x8a\xc9\x8a\xd7\x8a\xe5\x8a\xf3\x8a\x01\x8b\x0f\x8b\x1d\x8b+\x8b9\x8bG\x8bU\x8bc\x8bq\x8b\x7f\x8b\x8d\x8b\x9b\x8b\xa9\x8b\xb7\x8b\xc5\x8b\xd3\x8b\xe1\x8b\xef\x8b\xfd\x8b\x0b\x8c\x19\x8c'\x8c5\x8cC\x8cQ\x8c_\x8cm\x8c{\x8c\x89\x8c\x97\x8c\xa5\x8c\xb3\x8c\xc1\x8c\xcf\x8c\xdd\x8c\xeb\x8c\xf9\x8c\x07\x8d\x15\x8d#\x8d1\x8d?\x8dM\x8d[\x8di\x8dw\x8d\x85\x8d\x93\x8d\xa1\x8d\xaf\x8d\xbd\x8d\xcb\x8d\xd9\x8d\xe7\x8d\xf5\x8d\x03\x8e\x11\x8e\x1f\x8e-\x8e;\x8eI\x8eW\x8ee\x8es\x8e\x81\x8e\x8f\x8e\x9d\x8e\xab\x8e\xb9\x8e\xc7\x8e\xd5\x8e\xe3\x8e\xf1\x8e\xff\x8e\r\x8f\x1b\x8f)\x8f7\x8fE\x8fS\x8fa\x8fo\x8f}\x8f\x8b\x8f\x99\x8f\xa7\x8f\xb5\x8f\xc3\x8f\xd1\x8f\xdf\x8f\xed\x8f\xfb\x8f\t\x90\x17\x90%\x903\x90A\x90O\x90]\x90k\x90y\x90\x87\x90\x95\x90\xa3\x90\xb1\x90\xbf\x90\xcd\x90\xdb\x90\xe9\x90\xf7\x90\x05\x91\x13\x91!\x91/\x91=\x91K\x91Y\x91g\x91u\x91\x83\x91\x91\x91\x9f\x91\xad\x91\xbb\x91\xc9\x91\xd7\x91\xe5\x91\xf3\x91\x01\x92\x0f\x92\x1d\x92+\x929\x92G\x92U\x92c\x92q\x92\x7f\x92\x8d\x92\x9b\x92\xa9\x92\xb7\x92\xc5\x92\xd3\x92\xe1\x92\xef\x92\xfd\x92\x0b\x93\x19\x93'\x935\x93C\x93Q\x93_\x93m\x93{\x93\x89\x93\x97\x93\xa5\x93\xb3\x93\xc1\x93\xcf\x93\xdd\x93\xeb\x93\xf9\x93\x07\x94\x15\x94#\x941\x94?\x94M\x94[\x94i\x94w\x94\x85\x94\x93\x94\xa1\x94\xaf\x94\xbd\x94\xcb\x94\xd9\x94\xe7\x94"),
+ BasePatchData(address=1770357, patch_data=b'A!\x13"\x07=\x00\x00\x00\x10\xc0\x00\x00\x04\xc0\x00\x00\x04\x00\x00\x00\x00\x00\x00r\x00PR\xc0\x1d\x04\x06\x00\x14\x00\x00\x00\x00\x11\x00\x18\r\xc0\x07\x06\x19\x00\x00\x00\x00\x0c\x02\x12\x00\x00\x00\x00\x18\r&\x00&\x14\x00\x00\x00\xb5\x00\x08\x08\x00\x08\x08\x14\x00%\x00 \x06\x05\x0c\x00%\x00\x00\x08\x08\x14\x00%\x00 \x06\x05\x0c\x00%\x00 \x06\x05\x0c\x00%\x00\x00\x00\x17\x17\xc0\x07\x06\xc0\x07\x06\x07\x00\x00\x00\x00\x0f\x07\x19\x00\'\x00\x00\x0f\x07\x19\x00\'\x00\x00\x00KJJ\x00\x0f\x00\x0f\x07\x19\x00\'\x00\x00\x00\t:\x01\x0f\x07\x01\x0f\x07\x19\x00\x03\x00\x00\x00j\x1b\xc0(\x0e\xc0(\x0e\x13\x00\x00\x00\x00\x00\x00k\x8c\x8c@@\x1b\x0e\x18\x058\x00\x00\x13\x0b\x1c\x00\x08\x00\x00\x13\x0b\x1c\x00\x08\x00\x00\x00\x00\x1e\x00!\x13\x00!\x13"\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00!\x13"\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x08\x00\x00\x02\xc0\x1d\x04\xc0\x1d\x04\x06\x00\x00\x00\x00\x18\r&\x00\x00\x00\x00\x18\r&\x00\x00\x00\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\xc0\x18\r&\x00\x00\x00\xc0\x18\r&\x00\x00\x00\x00\x00\x00\xb6\x90\x08\x08\x90\x08\x08\x11\x03\x00\x00\x00\x00\x00f \x06\x05 \x06\x05\x19\x005\x00\x00\x00\'\x07\'\x01\x0f\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x01\x0f\x07\x19\x00\x00\x00\x00\x0f\x07\x19\x00\x16\x00\x00\x0f\x07\x19\x00\x16\x00\x00\x00\x00jjh\x0fh\x0f\x07\x08\x00\x03\x1c\x00\x00\x00\x0b\x00\x1a\x0e\x00\x1a\x0e\t\x00\x04?\x00\x00\x00\x8c\x00\x1b\x0e\x00\x1b\x0e\x18\x00\x00\x00\x00\x00\x00L \x13\x0b \x13\x0b\x1c\x00\x17\x00\x00\x00>\x0e\x00\x13\x0b\x00\x13\x0b)\x00\x17\x00\x00\x00\x00? \x0c\x02 \x0c\x02\x12\x00\x15%\x01\x01\x01\x01\x00\x00\x00\x01\x01\x01\x01\x00\x00\x00\x00\x18\r&\x00\x01\x00\x01\x01\x01\x01\x00\x00\x00\x00\x18\r&\x00\x01\x00\x00\x18\r&\x00\x00\x00\x00\x18\r\x1e\x00\x00\x00\x00\x18\r&\x00\x01\x00\x00\n\x08\x11\x00\x16\x00\x00\n\x08\x11\x00\x16\x00\x00\x00\x00vvv \x00\n\x08\x11\x00\x16\x00\x00\x00\x00vvv \x06\x05\x19\x006\x00\x00\x001\x171\x80\n\x80\n\x08\x11\x002\x1b\x00\x00\x008\xcc\x0e\t\xcc\x0e\t\x1a\x02%\x00\x00\x0f\x07\x19\x00\x00\x00\x00\x0f\x07\x19\x00\x00\x00\xc0\x0f\x07+\x00\x16\x00\xc0\x0f\x07+\x00\x16\x00\x00\x00\x00;\x00\x13\x0b\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00\x13\x0b\x1c\x00*\x00\xc0\x07\x06\x19\x00\x00\x00\x00\x13\x0b\x1c\x00*\x00\xc0\x07\x06\x19\x00\x00\x00\xc0\x07\x06\x19\x00\x00\x00\x00\x0c\x02\x12\x00\x00\x00\x00\x0c\x02\x12\x00\x00\x00\x00\x00\x00@ \x06\x05 \x06\x05\x19\x007\x04"\x00w\'w\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00B\x00\x04\x05\x00\x04\x05\x0b\x00\x15%\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x00\x00\x00T\x80\n\x08\x80\n\x08\x11\x00\x00\x19\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x19\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x00\x00\x00( \r\t \r\t\x13\x00\x00\x00\x00\x00) \x0f\x07\x19 \x0f\x07\x19\x00\x00\x00\x00\x00\n\n\x00\x0f\x07\x00\x0f\x07\x08\x00\x00\x00\x00\x00\x00+\x00\x07\x06\x00\x07\x06\x13\x00\x00\x00 \x1a\x0e\x0c\x003\x00 \x1a\x0e\x0c\x003\x00\x00\x00\x96\x96\xcc\x13\x0b\xcc\x13\x0b)\x02\x02\x00\x00\x00\x00\x1e\x00\x13\x0b\x00\x13\x0b)\x00\'\x14\x00\x00\x00\x1f_\xc0\x00\xc0\x00\x02\'\x00\x00\x00\x00\x00\x000\xb0\x01\x00\x01\x00\x00\x02\x00\x13\x00\x00\x00\x00B\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00A2h\x04h\x04\x05\n\x00\x00\x1d\x00\x17\n\x1b\x00\x01\x00\x00\x17\n\x1b\x00\x01\x00`\x17\n\x1b\x00\x01\x00`\x17\n\x1b\x00\x01\x00\x00\x00\x00\xbc\x00\n\x08\x00\n\x08\x11\x00<\x00\x00\r\t\x13\x0034\x00\r\t\x13\x0034\x00\x0f\x07\x19\x00\x17\x00\x00\r\t\x13\x0034\x00\x0f\x07\x19\x00\x17\x00\x00\r\t\x13\x0034\x00\x0f\x07\x19\x00\x17\x00\x00\x0f\x07\x19\x00\x17\x00\x00\x00\x00\t\t\x00\x0f\x00\x0f\x07\x08\x00\x01\x00\x00\x00\t\x00\x1a\x0e\x0c\x00\x1a\x0e\x0c\x00\x00\x00\x00\x00\x00\x1d \x1a\x0e \x1a\x0e\x0c\x002?\x00\x00\xa6\xa6\x00\x13\x0b\x00\x13\x0b)\x00\x17\x00\x00\x00\x00n\x00\x13\x0b\x00\x13\x0b\x1c\x00\x00\x00\x00\x00\xbe\xc0\x00\x00\x04\xc0\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\xc0\x00\x00\x03\x00\x00\x00\x00\x00\x00a\xc0\x00\x00\xc0\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\xc0\x04\x05\n\x00\x03\x00\x00\x00\x00c \n\x08 \n\x08\x11\x00\x00\x00\x00\x0044\x01\x01\x10\x01\x01\x10\r\x00\x00\x00\x00\r\t\x13\x00#\x00\x00\r\t\x13\x00#\x00\x00\r\t\x13\x00\x16\x00\x00\r\t\x13\x00\x16\x00\x00\r\t\x13\x00!(\x00\r\t\x13\x00!(\xc0\r\t\x13\x00\x00\x00\xc0\r\t\x13\x00\x00\x00\x00\x10\x07\x15\x00%\x00\x00\x10\x07\x15\x00%\x00\xc0\x1b\x0e\n\x00\x17\x00\xc0\x1b\x0e\n\x00\x17\x00\x00\x1b\x0e\n\x00\x00\x00\x00\x1b\x0e\n\x00\x00\x00\x00\x00\x00]\x00$\x0e\x00$\x0e#\x00\t\x00\x00\x00\x00\\ \x13\x0b \x13\x0b\x1c\x00\x00\x00\x00\x00~~\x00\x13\x0b\x00\x13\x0b\x1c\x00\'\x00\x00\x00\x00?\x7f\xc0\x00\xc0\x00\x00\x04\x00\x00\x00\xc0\x00\x00\x04\x00\x00\x00\xc0\x00\x00\x04\x00\x00\x00\x08\x00\x00Q\x00\t\x05\xc0\x00\x00\x04\x00\x00\x00\xc0\x00\x00\x04\x00\x00\x00\x00\t\x05\n\x00\r\x00\x00\x00\x00S\xe0#\n\xe0#\n!\x00\x17\x00\x00\x00\x00\xab\xe0#\n\xe0#\n!\x00\x00\x00\x00\x00\xac\xc0\n\x08\x11\xc0\n\x08\x11\x00<\x00\x00\x00\x00\x16\x00\r\t\x00\r\t\x13\x00"\x00\x00\r\t\x13\x00\x00\x00\x00\r\t\x13\x00\x00\x00\x01\x0f\x07\x19\x00\x00\x00\x01\x0f\x07\x19\x00\x00\x00\x00\x00\x00\x1a\x1a\x00\x1b\x01\x0f\x07\x19\x00\x00\x00\x00\x00\x00\x1a\x1a\x00\x1b\x00\x1b\x0e\n\x00\x08\x0b\x00\x00\x00\x0c\x00$\x0e\x00$\x0e#\x00\x03?\x00\x00\x00\xa5\x00$\x0e\x00$\x0e#\x00\x05\x00\x00\x13\x0b\x1c\x00\x02\x00\x00\x13\x0b\x1c\x00\x02\x00\x00\x00\x00N\x00\x01\x01\x00\x01\x01\x04\x00\x00\x00\x08\x00\x00q\x80\xc0\x01\x00\x01\x01\x04\x00\x00\x00\x08\x00\x00q\x80\xc0\x01\xc0\x01\x01\x04\x00\x08\x00\x00\x00\x00p\xc0\x01\x01\xc0\x01\x01\x04\x00\x00\x00\x08\x00\x00\x01\x00\t\x05\x00\t\x05\n\x00\x17\x00\x00\t\x05\n\x00\'\x00\x00\t\x05\n\x00\'\x00\x00\t\x05\n\x00\x01\x00\x00\t\x05\n\x00\x01\x00\x80\n\x08\x11\x00\x00\x18\x80\n\x08\x11\x00\x00\x18\x00\x00\x00&&&\xc0\xc0\x06\x05\x19\x00\x00\x00\x00\x00\xa71\x87\x87\x00\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00 \x00\x00(\x0e\x13\x00\x04<\x00(\x0e\x13\x00\x04<\x00\x00\x9b \x13\x0b\x1c \x13\x0b\x1c\x00+\x17\x00\x00\x9e^\x00\x13\x0b\x00\x13\x0b\x1c\x00\x00\x00\x00\x00\x00_`\x01\x01`\x01\x01\x04\x00\x00\x00\x00\x00\x00p\xc0\x01\x01\xc0\x01\x01\x04\x00\x00\x00\x00\t\x05\n\x00\r\x00\xc0\x01\x01\x04\x00\x00\x00\x00\t\x05\n\x00\r\x00\x00\t\x05\n\x00\r\x00\x00\t\x05\n\x00\x00\x00\x00\t\x05\n\x00\x00\x00\x00\t\x05\n\x00\x02\x00\x00\t\x05\n\x00\x02\x00\x00\x06\x05\x19\x00>\x01\x00\x06\x05\x19\x00>\x01(\x00\x00ww\x00\x0b\x00\x06\x05\x19\x00>\x01(\x00\x00ww\x00\x0b\x00\x0b\x05\x08\x00\x00\x00\x02\x00\xa9\x00(\x0e\x13\x00\x0b\x05\x08\x00\x00\x00\x02\x00\xa9\x00(\x0e\x13\x00(\x0e\x13\x00:\x0c (\x0e\x13\x00\x16\x00\x00(\x0e\x13\x00:\x0c (\x0e\x13\x00\x16\x00 (\x0e\x13\x00\x16\x00(\x00\x1c\x0c\x0c\x1c\x00\x00(\x0e\x13\x003)\x00\x13\x0b\x1c\x00\x00\x00\x00\x13\x0b\x1c\x00\x00\x00\x00\x00\x00\xae\x80\x12\x0c\x80\x12\x0c\x16\x00%\x00\x00\x11\x0c\x1c\x00\x00\x00\x80\x12\x0c\x16\x00%\x00\x00\x11\x0c\x1c\x00\x00\x00\x00\x11\x0c\x1c\x00\x00\x00\x00\x00\x00\xa0\x01\x11\x0c\x01\x11\x0c\x1c\x00\x00\x00\x01\x11\x0c\x1c\x00\x16\x00\x01\x11\x0c\x1c\x00\x16\x00\x08\x00\x00\xa2\x00%\x0e\x00%\x0e$\x00\x00\x00\x00%\x0e$\x003\x00\x00%\x0e$\x00\x00\x00\x00%\x0e$\x003\x00\x00%\x0e$\x003\x00\x00\x00\x00=h\x11\x0ch\x11\x0c\x1d\x00\x1c\x00\x00\x00\xd1\xd1\x00\x11\x0c\x00\x11\x0c\x1c\x00\x00\x00\x00\x00\x00\xd2\x01\x0b\x05\x01\x0b\x05\x08\x00\x00\x00\x00\x00\x00\xda\x00(\x0e\x00(\x0e\x13\x00\x00\x00\x00\x00}\x00(\x0e\x13\x00(\x0e\x13\x00\x00\x00\x00\x00}\x00(\x0e\x13\x00(\x0e\x13\x06\x00\x00\x00(\x0e\x13\x06\x00;\x00(\x0e\x13\x06\x00;\x00\x00{ \x13\x0b\x1c \x13\x0b\x1c\x00\x00\x00\x00\x00\xbe\xbe\x00\x13\x0b\x00\x13\x0b\x1c\x00\x17\x00\x00\x12\x0c\x1d\x00\x00\x00\x00\x12\x0c\x1d\x00\x00\x00\x00\x00\x00\x91\x00\x11\x0c\x00\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00\x00\x00\x00\x93`\x19\r\x00\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00`\x19\r\x17\x04%\x00\x00%\x0e$\x00\x07\x00\x00%\x0e$\x00\x07\x00\x00\x00\x00l\x00%\x0e\x00%\x0e$\x00\x00\x00\x00\x00\x00M\x00\x06\x05\x00\x06\x05\x19\x00\x00\x00\x00\x00\x17\xc0\x0b\x05\x08\xc0\x0b\x05\x08\x00\x03\x00\xc0\x0b\x05\x08\x00\x17\x00\xc0\x0b\x05\x08\x00\x17\x00\x00\x00\x89\xc0\x0b\x05\x08\xc0\x0b\x05\x08\x00\x17\x00\x00\x17\n\x1b\x00\x00\x00\x00\x17\n\x1b\x00\x00\x00\x00\x00\x00d\xe0\x17\n\xe0\x17\n \x00%\x00\x00\x13\x0b\x1c\x00\'\x00\x00\x13\x0b\x1c\x00\'\x00\x00\x00\x00\x8e\x00\x13\x0b\x00\x13\x0b\x1c\x00\'\x00\x00\x00\x00\x8e\x00\x13\x0b\x00\x13\x0b\x1c\x00\x00\x00\x00&\x02!\x00\x05\x02\x00&\x02!\x00\x05\x02\x08\x00\x00@\xc0\x00\x11\x00\x11\x0c\x1d\x00\x00\x00\x02\x00\xb2\xc0\x11\x0c\x1d\xc0\x11\x0c\x1d\x00\x03\x0e\xc0\x11\x0c\x1d\x00\'\x00\xc0\x11\x0c\x1d\x00\'\x00\x00\x19\r\x17\x00\x00\x00\x00\x19\r\x17\x00\x00\x00\x00\x00\x00\xc4\x01\x18\r\x01\x18\r%\x00\x17\x00\x00\x00\x00\x04\x00\x18\r\x00\x18\r\x1e\x00\x04<\x00\x00\x00\x15\x00\x0b\x05\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\x00\x0b\x05\x08\x00\'\x00\xc0\x0b\x05\x08\x00\x00\x00\xc0\x0b\x05\x08\x00\x00\x00\x01\x0b\x05\x08\x00\x17\x00\x01\x0b\x05\x08\x00\x17\x00@\x17\n\x1b\x00\x00\x00@\x17\n\x1b\x00\x00\x00\x00\x17\n\x1b\x00\x17\x00\x00\x17\n\x1b\x00\x17\x00\x00\x00\x00E\x00\x13\x0b\x00\x13\x0b)\x00\x16\x00\x00\x00O\x9e\x00\x13\x0b\x00\x13\x0b)\x00\x16\x00\x00\x00O\x9e\x00\x13\x0b\x00\x13\x0b)\x00\x00\x00\x01\x00\x02\'\x00\x02\x0f\x01\x00\x02\'\x00\x02\x0f\x00\x00\x00\xb0\xd0\x00\x11\x00\x11\x0c\x1d\x003\x00\xc0\x11\x0c\x1d\x00\'\x00\xc0\x11\x0c\x1d\x00\'\x00\xc0\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00\x00\x18\r%\x00\x00\x00\x00\x18\r%\x00\x00\x00\x00\x00\x00\xb4\x00\x18\r\x00\x18\r%\x00\x00\x00\x00\x18\r\x1e\x003\x00\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\x00\x18\r\x1e\x003\x00\x00\x0b\x05\t\x00\x15%\x00\x0b\x05\t\x00\x15%\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\xc0\x17\n\x1b\x00\x00\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00 \x13\x0b)\x00\x14\x00\x00\x00\xde\x01\x00\x02! \x13\x0b)\x00\x14\x00\x00\x00\xde\x01\x00\x02!\x01\x00\x02!\x00\x0f\x00\x00\x00\x00\xc0\xe0\x00\x11\x01\x00\x02!\x00\x0f\x00\x00\x00\x00\xc0\xe0\x00\x11\x00\x11\x0c\x1d\x00\x00\x00\x00\x00\xb1\x97\x00\x11\x0c\x00\x11\x0c\x1d\x00\n\x00\x00\x00\x00\x98\x00\x0b\x05\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x18\r%\x00\x00\x00\x00\x18\r\x1e\x003\x00\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x00\x00\x99\xe0\x14\x0b\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xe0\x14\x0b\x16\x00%\x00\xc0 \x06\x13\x00\x00\x00\xe0\x14\x0b\x16\x00%\x00\xc0 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\x00\x00\x00\xef\x00&\x02\x00&\x02!\x00\x01*\x00\x00\x00\xd0\xc0\x07\x06\xc0\x07\x06(\x00\x00\x00\x00 \x06\x13\x00\x00\x00\x00 \x06\x13\x00\x00\x00\xc0 \x06\t\x00\x00\x00\xc0 \x06\t\x00\x00\x00\x01\x07\x14\x01\x00\x00\x00\x01\x07\x14\x01\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00\x01\x07\x14\x01\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x00\x00\xf8\xf8\xf8\xf8\xf8 \x06\x13\x00\x00\x00\x00\x00\xfa\xfa \x07\x06 \x06\x13\x00\x00\x00\x00\x00\xfa\xfa \x07\x06 \x07\x06\x19\x00\x00\x00\x00\x00\xfb\xfb \x06 \x06\x13\x00\x00\x00\x00\x00\xfd\xfd\xfd \x06\x13\x00\x00\x00\x00\x00\xfd\xfd\xfd \x06\x13\x00\x00\x00\x00\x00\xfe \x06\x13 \x06\x13\x00\x02\x00\x08\x00\xff\xdf\xff\x00\x02\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x00\x02\x03\x05\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x00\x02\x03\x05\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x06\x13\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x06\x13\x00\x00\x00\x00\x07\x06\x13\x00\x00\x00\x00\x00\x00\xe8\xe8\xe8\xe8\x00\x07\x06\x13\x00\x00\x00\x00\x00\x00\xe8\xe8\xe8\xe8\x00\x07\x06\x13\x00\x00\x00\x00\x00\x00\xe8\xe8\xe8\xe8\x00 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\x00\x00\x00\xea\x00\x07\x06\x00\x07\x06\x19\x00\x00\x00\x00\x00\x00\xeb\x00 \x06\x00 \x06\x13\x00\x00\x00\x00\x00\x00\xed\xed\x00\x07\x00 \x06\x13\x00\x00\x00\x00\x00\x00\xed\xed\x00\x07\x00 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\x00\x07\x06\x05\x00\x00\x00\x00\x00\x00\xef\x00\x05\x03\x00\x05\x03(\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x15\x03\r\x00\x00\x00\x00\x15\x03\r\x00\x00\x00\x00\x05\x03\x0f\x00\x00\x00\x00\x05\x03\x0f\x00\x00\x00\x01\x15\x03\r\x00\x00\x00\x01\x15\x03\r\x00\x00\x00\x00\x1c\x0f\x10\x00\x00\x00\x00\x1c\x0f\x10\x00\x00\x00\x00\x1f\x03\x0f\x00\x00\x00\x00\x1f\x03\x0f\x00\x00\x00\x00\x02\x03\x01\x00\x00\x00\x00\x02\x03\x01\x00\x00\x00\x00\x02\x03\x0e\x00\x00\x00\x00\x02\x03\x0e\x00\x00\x00\x01\x05\x03\x05\x00\x00\x00\x01\x05\x03\x05\x00\x00\x00\x01\x07\x06\x10\x00\x00\x00\x01\x07\x06\x10\x00\x00\x00\x80\n\x08\x08\x00\x00\x1a\x80\n\x08\x08\x00\x00\x1a\x00\'\x06\x08\x00\x03\x00\x00\'\x06\x08\x00\x03\x00\x00\n\x08\x11\x00\x00\x00\x00\n\x08\x11\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x03\x10\x08\x00\x00\x00\x00\x03\x10\x08\x00\x00\x00\x00\x07\x06\x07\x00\x00\x00\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00 \x14\x05\x00\x00\x00\x00 \x14\x05\x00\x00\x00\xe0#\n\x0f\x00\x00\x00\x00\x05\x03\x0f\x00\x00\x00\x01\x15\x03\r\x00\x00\x00\xe0#\n\x0f\x00\x00\x00\x00\x00\x00\x1d\x00\x1c\x0f\x00\x1c\x0f\x05\x00\x00\x00\xc0\x07\x06\x08\x00\x00\x00\xc0\x07\x06\x08\x00\x00\x00\x00#\n\x0f\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00#\n\x0f\x00\x00\x00\x00\x00\x00\x19\x00 \x06\x00 \x06*\x00\x00\x00\x00\x05\x03\x05\x00\x00\x00\x00\x05\x03\x05\x00\x00\x00\x00\x13\x06\x13\x00\x00\x00\x00\x13\x06\x13\x00\x00\x00\x00\x07\x06(\x00\x03\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x07\x06(\x00\x03\x00\x00\x07\x06(\x00\x00\x00\x00\x07\x06(\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x07\x06(\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x07\x06(\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00 \x06*\x00\x00\x00\x00\x05\x03\x05\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'),
+)
diff --git a/worlds/alttp/enemizer_data/dungeon_sprite_addresses.py b/worlds/alttp/enemizer_data/dungeon_sprite_addresses.py
new file mode 100644
index 000000000000..af739b103ae9
--- /dev/null
+++ b/worlds/alttp/enemizer_data/dungeon_sprite_addresses.py
@@ -0,0 +1,202 @@
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class DungeonSpriteAddressData(NamedTuple):
+ room_id: int
+ sprite_id_addresses: tuple[int, ...]
+
+DUNGEON_SPRITE_ADDRESSES = (
+ DungeonSpriteAddressData(room_id=2, sprite_id_addresses=(317750, 317753, 317756, 317759, 317762, 317792, 317795)),
+ DungeonSpriteAddressData(room_id=4, sprite_id_addresses=(317803, 317806, 317809, 317812, 317827, 317839, 317842, 317845)),
+ DungeonSpriteAddressData(room_id=9, sprite_id_addresses=(317904, 317907, 317910)),
+ DungeonSpriteAddressData(room_id=10, sprite_id_addresses=(317915, 317918, 317921, 317924, 317930, 317933)),
+ DungeonSpriteAddressData(room_id=11, sprite_id_addresses=(317941, 317944, 317947, 317950, 317953, 317956, 317959, 317962, 317965)),
+ DungeonSpriteAddressData(room_id=14, sprite_id_addresses=(317978, 317981, 317984)),
+ DungeonSpriteAddressData(room_id=17, sprite_id_addresses=(317992, 317995, 317998, 318001, 318004, 318007, 318010, 318013)),
+ DungeonSpriteAddressData(room_id=19, sprite_id_addresses=(318029, 318032, 318035, 318038, 318044, 318056, 318053, 318041)),
+ DungeonSpriteAddressData(room_id=21, sprite_id_addresses=(318105, 318108, 318111, 318114, 318117, 318120)),
+ DungeonSpriteAddressData(room_id=22, sprite_id_addresses=(318125, 318128, 318131, 318134, 318137, 318140, 318143)),
+ DungeonSpriteAddressData(room_id=23, sprite_id_addresses=(318157, 318160, 318163, 318166, 318169, 318172)),
+ DungeonSpriteAddressData(room_id=25, sprite_id_addresses=(318177, 318180, 318183, 318186)),
+ DungeonSpriteAddressData(room_id=26, sprite_id_addresses=(318191, 318194, 318197, 318200, 318203, 318206, 318209, 318212, 318218)),
+ DungeonSpriteAddressData(room_id=27, sprite_id_addresses=(318232, 318235, 318238, 318241)),
+ DungeonSpriteAddressData(room_id=30, sprite_id_addresses=(318284, 318287, 318290, 318293, 318296, 318299)),
+ DungeonSpriteAddressData(room_id=31, sprite_id_addresses=(318304, 318307, 318310, 318313, 318316, 318319, 318322, 318325)),
+ DungeonSpriteAddressData(room_id=33, sprite_id_addresses=(318335, 318341, 318344, 318347, 318350, 318353, 318356, 318359, 318362, 318365, 318368)),
+ DungeonSpriteAddressData(room_id=34, sprite_id_addresses=(318373, 318376, 318379, 318382, 318385, 318388, 318391)),
+ DungeonSpriteAddressData(room_id=36, sprite_id_addresses=(318413, 318416, 318419, 318422, 318425, 318428, 318431)),
+ DungeonSpriteAddressData(room_id=38, sprite_id_addresses=(318471, 318438, 318441, 318444, 318447, 318450, 318453, 318459, 318462, 318465, 318468)),
+ DungeonSpriteAddressData(room_id=39, sprite_id_addresses=(318476, 318479, 318482, 318485, 318488, 318491, 318494)),
+ DungeonSpriteAddressData(room_id=40, sprite_id_addresses=(318511,)),
+ DungeonSpriteAddressData(room_id=42, sprite_id_addresses=(318530, 318533, 318536, 318539, 318542, 318545)),
+ DungeonSpriteAddressData(room_id=43, sprite_id_addresses=(318556, 318559, 318562, 318565, 318568, 318571)),
+ DungeonSpriteAddressData(room_id=46, sprite_id_addresses=(318590, 318593, 318596, 318599, 318602, 318605)),
+ DungeonSpriteAddressData(room_id=49, sprite_id_addresses=(318621, 318624, 318627, 318630, 318633, 318636, 318639, 318642, 318645, 318648)),
+ DungeonSpriteAddressData(room_id=50, sprite_id_addresses=(318653, 318656, 318659, 318662, 318665)),
+ DungeonSpriteAddressData(room_id=52, sprite_id_addresses=(318681, 318684, 318687, 318693, 318696, 318699, 318690)),
+ DungeonSpriteAddressData(room_id=53, sprite_id_addresses=(318710, 318713, 318716, 318719, 318722, 318728, 318731, 318734, 318725)),
+ DungeonSpriteAddressData(room_id=54, sprite_id_addresses=(318742, 318745, 318754, 318760, 318763)),
+ DungeonSpriteAddressData(room_id=55, sprite_id_addresses=(318777, 318780, 318783, 318786, 318792, 318795, 318798, 318801, 318789)),
+ DungeonSpriteAddressData(room_id=56, sprite_id_addresses=(318806, 318809, 318812, 318815, 318818, 318821, 318824)),
+ DungeonSpriteAddressData(room_id=57, sprite_id_addresses=(318829, 318835, 318841, 318844, 318847, 318850)),
+ DungeonSpriteAddressData(room_id=58, sprite_id_addresses=(318855, 318858, 318861, 318864, 318867, 318870)),
+ DungeonSpriteAddressData(room_id=59, sprite_id_addresses=(318875, 318878, 318881, 318884, 318887, 318890, 318893)),
+ DungeonSpriteAddressData(room_id=60, sprite_id_addresses=(318898, 318901, 318904)),
+ DungeonSpriteAddressData(room_id=61, sprite_id_addresses=(318915, 318921, 318924, 318927, 318930, 318933, 318939, 318942, 318945, 318948, 318951)),
+ DungeonSpriteAddressData(room_id=62, sprite_id_addresses=(318959, 318962, 318980, 318983, 318989, 318992)),
+ DungeonSpriteAddressData(room_id=63, sprite_id_addresses=(319000, 319006, 319009)),
+ DungeonSpriteAddressData(room_id=64, sprite_id_addresses=(319014, 319017, 319023, 319026, 319029)),
+ DungeonSpriteAddressData(room_id=65, sprite_id_addresses=(319036, 319039, 319042, 319045)),
+ DungeonSpriteAddressData(room_id=66, sprite_id_addresses=(319050, 319053, 319056, 319059, 319062, 319065)),
+ DungeonSpriteAddressData(room_id=67, sprite_id_addresses=(319070, 319073)),
+ DungeonSpriteAddressData(room_id=68, sprite_id_addresses=(319084, 319087, 319090, 319093, 319096, 319102)),
+ DungeonSpriteAddressData(room_id=69, sprite_id_addresses=(319110, 319116, 319119, 319131, 319134, 319137, 319113, 319122, 319125, 319128)),
+ DungeonSpriteAddressData(room_id=70, sprite_id_addresses=(319142, 319148, 319154)),
+ DungeonSpriteAddressData(room_id=73, sprite_id_addresses=(319161, 319164, 319167, 319170, 319173, 319176, 319182, 319185, 319188, 319191, 319194, 319197)),
+ DungeonSpriteAddressData(room_id=74, sprite_id_addresses=(319205, 319208)),
+ DungeonSpriteAddressData(room_id=75, sprite_id_addresses=(319213, 319216, 319219, 319222, 319225, 319228, 319231, 319234)),
+ DungeonSpriteAddressData(room_id=76, sprite_id_addresses=(319245, 319248, 319251, 319254, 319257, 319260)),
+ DungeonSpriteAddressData(room_id=78, sprite_id_addresses=(319270, 319273, 319276, 319279)),
+ DungeonSpriteAddressData(room_id=80, sprite_id_addresses=(319295, 319298, 319301)),
+ DungeonSpriteAddressData(room_id=81, sprite_id_addresses=(319309, 319312)),
+ DungeonSpriteAddressData(room_id=82, sprite_id_addresses=(319317, 319320, 319323)),
+ DungeonSpriteAddressData(room_id=83, sprite_id_addresses=(319328, 319331, 319334, 319337, 319340, 319343, 319346, 319349, 319352, 319355, 319358, 319361, 319364)),
+ DungeonSpriteAddressData(room_id=84, sprite_id_addresses=(319369, 319372, 319375, 319378, 319381, 319384, 319387, 319390)),
+ DungeonSpriteAddressData(room_id=85, sprite_id_addresses=(319398, 319401)),
+ DungeonSpriteAddressData(room_id=86, sprite_id_addresses=(319415, 319418, 319421, 319424, 319430, 319433, 319436, 319442, 319439)),
+ DungeonSpriteAddressData(room_id=87, sprite_id_addresses=(319447, 319450, 319453, 319456, 319459, 319462, 319468, 319471, 319474, 319477, 319480, 319483, 319486, 319489)),
+ DungeonSpriteAddressData(room_id=88, sprite_id_addresses=(319497, 319500, 319506, 319509, 319515, 319518, 319521)),
+ DungeonSpriteAddressData(room_id=89, sprite_id_addresses=(319526, 319529, 319538, 319544, 319547, 319550, 319553, 319556, 319559, 319541)),
+ DungeonSpriteAddressData(room_id=91, sprite_id_addresses=(319575, 319578, 319581, 319584)),
+ DungeonSpriteAddressData(room_id=93, sprite_id_addresses=(319615, 319618, 319621, 319624, 319627, 319633, 319636, 319639, 319651, 319642, 319645, 319648, 319630)),
+ DungeonSpriteAddressData(room_id=94, sprite_id_addresses=(319659, 319662, 319665, 319668)),
+ DungeonSpriteAddressData(room_id=95, sprite_id_addresses=(319673, 319676, 319679)),
+ DungeonSpriteAddressData(room_id=96, sprite_id_addresses=(319684,)),
+ DungeonSpriteAddressData(room_id=97, sprite_id_addresses=(319689, 319692, 319695)),
+ DungeonSpriteAddressData(room_id=98, sprite_id_addresses=(319700, 319703, 319706)),
+ DungeonSpriteAddressData(room_id=99, sprite_id_addresses=(319714, 319711)),
+ DungeonSpriteAddressData(room_id=100, sprite_id_addresses=(319719, 319725, 319728, 319731, 319734, 319737)),
+ DungeonSpriteAddressData(room_id=101, sprite_id_addresses=(319760, 319763, 319766, 319769, 319772)),
+ DungeonSpriteAddressData(room_id=102, sprite_id_addresses=(319777, 319783, 319786, 319795, 319798, 319801, 319804, 319810)),
+ DungeonSpriteAddressData(room_id=103, sprite_id_addresses=(319818, 319821, 319824, 319827, 319830, 319833, 319836, 319839, 319842)),
+ DungeonSpriteAddressData(room_id=104, sprite_id_addresses=(319859, 319865, 319868)),
+ DungeonSpriteAddressData(room_id=106, sprite_id_addresses=(319873, 319876, 319879, 319882, 319885, 319888)),
+ DungeonSpriteAddressData(room_id=107, sprite_id_addresses=(319899, 319902, 319905, 319911, 319914, 319917, 319920, 319923, 319926, 319929, 319932)),
+ DungeonSpriteAddressData(room_id=109, sprite_id_addresses=(319954, 319957, 319960, 319963, 319966, 319969, 319972, 319975, 319978)),
+ DungeonSpriteAddressData(room_id=110, sprite_id_addresses=(319983, 319986, 319989, 319992, 319995)),
+ DungeonSpriteAddressData(room_id=113, sprite_id_addresses=(320000, 320003)),
+ DungeonSpriteAddressData(room_id=114, sprite_id_addresses=(320011, 320017)),
+ DungeonSpriteAddressData(room_id=115, sprite_id_addresses=(320022, 320025, 320028, 320031, 320034, 320037)),
+ DungeonSpriteAddressData(room_id=116, sprite_id_addresses=(320045, 320048, 320051, 320054, 320057, 320060, 320063, 320066)),
+ DungeonSpriteAddressData(room_id=117, sprite_id_addresses=(320071, 320074, 320077, 320080, 320083, 320086, 320095, 320098)),
+ DungeonSpriteAddressData(room_id=118, sprite_id_addresses=(320106, 320109, 320112, 320115, 320121)),
+ DungeonSpriteAddressData(room_id=119, sprite_id_addresses=(320126, 320138, 320141)),
+ DungeonSpriteAddressData(room_id=123, sprite_id_addresses=(320146, 320149, 320152, 320155, 320158, 320161, 320167, 320170, 320173, 320176)),
+ DungeonSpriteAddressData(room_id=124, sprite_id_addresses=(320181, 320184, 320187, 320190, 320193, 320196)),
+ DungeonSpriteAddressData(room_id=125, sprite_id_addresses=(320216, 320219, 320225, 320228, 320234, 320222, 320231, 320204, 320207, 320210, 320213, 320222, 320231)),
+ DungeonSpriteAddressData(room_id=126, sprite_id_addresses=(320242, 320245, 320254, 320257)),
+ DungeonSpriteAddressData(room_id=128, sprite_id_addresses=(320291, 320294)),
+ DungeonSpriteAddressData(room_id=129, sprite_id_addresses=(320302, 320305)),
+ DungeonSpriteAddressData(room_id=130, sprite_id_addresses=(320310, 320313, 320316)),
+ DungeonSpriteAddressData(room_id=131, sprite_id_addresses=(320321, 320324, 320327, 320330, 320333, 320336, 320339, 320342, 320345, 320348)),
+ DungeonSpriteAddressData(room_id=132, sprite_id_addresses=(320353, 320356, 320359, 320362, 320365, 320368, 320371)),
+ DungeonSpriteAddressData(room_id=133, sprite_id_addresses=(320376, 320379, 320382, 320385, 320388, 320391, 320394, 320397, 320400, 320403)),
+ DungeonSpriteAddressData(room_id=135, sprite_id_addresses=(320410, 320413, 320416, 320419, 320434, 320437, 320440, 320446, 320422)),
+ DungeonSpriteAddressData(room_id=139, sprite_id_addresses=(320468, 320471, 320474, 320477, 320480)),
+ DungeonSpriteAddressData(room_id=140, sprite_id_addresses=(320503, 320506, 320509, 320512, 320518, 320521, 320527, 320524, 320515)),
+ DungeonSpriteAddressData(room_id=141, sprite_id_addresses=(320538, 320541, 320544, 320547, 320550, 320556, 320559, 320562, 320565, 320568, 320571, 320535)),
+ DungeonSpriteAddressData(room_id=142, sprite_id_addresses=(320579, 320582, 320585, 320588, 320591, 320594, 320597)),
+ DungeonSpriteAddressData(room_id=145, sprite_id_addresses=(320610, 320616, 320619, 320622, 320625, 320613)),
+ DungeonSpriteAddressData(room_id=146, sprite_id_addresses=(320636, 320639, 320642, 320645, 320648, 320654, 320657, 320660, 320663)),
+ DungeonSpriteAddressData(room_id=147, sprite_id_addresses=(320668, 320671, 320674, 320677, 320680, 320683, 320686, 320689)),
+ DungeonSpriteAddressData(room_id=149, sprite_id_addresses=(320694, 320697, 320700, 320703)),
+ DungeonSpriteAddressData(room_id=151, sprite_id_addresses=(320728,)),
+ DungeonSpriteAddressData(room_id=152, sprite_id_addresses=(320733, 320736, 320739, 320742, 320745)),
+ DungeonSpriteAddressData(room_id=153, sprite_id_addresses=(320750, 320753, 320756, 320759, 320765, 320768, 320771, 320774, 320777, 320780)),
+ DungeonSpriteAddressData(room_id=155, sprite_id_addresses=(320794, 320797, 320800, 320803, 320806, 320809, 320812, 320815, 320818, 320821)),
+ DungeonSpriteAddressData(room_id=156, sprite_id_addresses=(320826, 320829, 320832, 320835, 320838, 320841)),
+ DungeonSpriteAddressData(room_id=157, sprite_id_addresses=(320852, 320855, 320858, 320861, 320864, 320867, 320870, 320873)),
+ DungeonSpriteAddressData(room_id=158, sprite_id_addresses=(320878, 320881, 320884, 320887)),
+ DungeonSpriteAddressData(room_id=159, sprite_id_addresses=(320907, 320910)),
+ DungeonSpriteAddressData(room_id=160, sprite_id_addresses=(320915, 320918, 320921)),
+ DungeonSpriteAddressData(room_id=161, sprite_id_addresses=(320929, 320932, 320935, 320938, 320941, 320944, 320947, 320950)),
+ DungeonSpriteAddressData(room_id=165, sprite_id_addresses=(320968, 320971, 320974, 320977, 320980, 320983, 320986, 320989, 320998, 321001)),
+ DungeonSpriteAddressData(room_id=167, sprite_id_addresses=(321014, 321017)),
+ DungeonSpriteAddressData(room_id=168, sprite_id_addresses=(321022, 321025, 321028, 321031, 321034)),
+ DungeonSpriteAddressData(room_id=169, sprite_id_addresses=(321039, 321042, 321057, 321060, 321045, 321048, 321051, 321054)),
+ DungeonSpriteAddressData(room_id=170, sprite_id_addresses=(321065, 321068, 321071, 321074, 321077, 321080)),
+ DungeonSpriteAddressData(room_id=171, sprite_id_addresses=(321088, 321091, 321094, 321097, 321100, 321103, 321106)),
+ DungeonSpriteAddressData(room_id=174, sprite_id_addresses=(321116, 321119)),
+ DungeonSpriteAddressData(room_id=176, sprite_id_addresses=(321129, 321132, 321135, 321138, 321141, 321144, 321147, 321150, 321153, 321156, 321159, 321165, 321168)),
+ DungeonSpriteAddressData(room_id=177, sprite_id_addresses=(321173, 321176, 321179, 321182, 321185, 321188, 321191, 321194, 321197, 321200)),
+ DungeonSpriteAddressData(room_id=178, sprite_id_addresses=(321205, 321208, 321211, 321214, 321217, 321220, 321223, 321226, 321229, 321232, 321235, 321238, 321241, 321244)),
+ DungeonSpriteAddressData(room_id=179, sprite_id_addresses=(321249, 321252, 321255, 321258, 321261)),
+ DungeonSpriteAddressData(room_id=182, sprite_id_addresses=(321277, 321280, 321289, 321292, 321301, 321304)),
+ DungeonSpriteAddressData(room_id=183, sprite_id_addresses=(321309, 321312)),
+ DungeonSpriteAddressData(room_id=184, sprite_id_addresses=(321317, 321320, 321323, 321326, 321329, 321332)),
+ DungeonSpriteAddressData(room_id=186, sprite_id_addresses=(321342, 321345, 321348, 321351, 321354, 321357, 321360)),
+ DungeonSpriteAddressData(room_id=187, sprite_id_addresses=(321365, 321368, 321371, 321374, 321377, 321380, 321386, 321389, 321392, 321395, 321383)),
+ DungeonSpriteAddressData(room_id=188, sprite_id_addresses=(321403, 321406, 321409, 321412, 321418, 321421, 321424, 321433, 321400, 321415, 321427, 321430)),
+ DungeonSpriteAddressData(room_id=190, sprite_id_addresses=(321440, 321446, 321449, 321452, 321455, 321458)),
+ DungeonSpriteAddressData(room_id=192, sprite_id_addresses=(321471, 321474, 321477, 321480, 321486, 321489, 321492, 321495)),
+ DungeonSpriteAddressData(room_id=193, sprite_id_addresses=(321503, 321506, 321509, 321512, 321518, 321524, 321527, 321530, 321521, 321515, 321536)),
+ DungeonSpriteAddressData(room_id=194, sprite_id_addresses=(321547, 321550, 321553, 321556, 321562, 321559, 321544, 321541)),
+ DungeonSpriteAddressData(room_id=195, sprite_id_addresses=(321567, 321585, 321588)),
+ DungeonSpriteAddressData(room_id=196, sprite_id_addresses=(321605, 321608, 321611, 321614, 321617, 321620)),
+ DungeonSpriteAddressData(room_id=201, sprite_id_addresses=(321697, 321700, 321703)),
+ DungeonSpriteAddressData(room_id=203, sprite_id_addresses=(321708, 321717, 321720, 321723, 321726, 321729, 321732, 321735, 321738, 321741, 321714, 321711)),
+ DungeonSpriteAddressData(room_id=204, sprite_id_addresses=(321746, 321749, 321755, 321758, 321761, 321770, 321773, 321776, 321779, 321782, 321785, 321752, 321764, 321767)),
+ DungeonSpriteAddressData(room_id=206, sprite_id_addresses=(321790, 321793, 321799, 321802, 321805, 321808, 321811)),
+ DungeonSpriteAddressData(room_id=208, sprite_id_addresses=(321816, 321819, 321822, 321825, 321828, 321831, 321834, 321837, 321840, 321843, 321846)),
+ DungeonSpriteAddressData(room_id=209, sprite_id_addresses=(321851, 321854, 321857, 321860, 321863, 321866, 321869, 321872)),
+ DungeonSpriteAddressData(room_id=210, sprite_id_addresses=(321877, 321880, 321883, 321886, 321889, 321892, 321895, 321898, 321901, 321904)),
+ DungeonSpriteAddressData(room_id=216, sprite_id_addresses=(321937, 321940, 321943, 321946, 321949, 321952, 321955, 321958, 321961, 321964, 321967)),
+ DungeonSpriteAddressData(room_id=217, sprite_id_addresses=(321975, 321978, 321981, 321972)),
+ DungeonSpriteAddressData(room_id=218, sprite_id_addresses=(321986, 321989)),
+ DungeonSpriteAddressData(room_id=219, sprite_id_addresses=(321994, 321997, 322000, 322006, 322003, 322012, 322009)),
+ DungeonSpriteAddressData(room_id=220, sprite_id_addresses=(322020, 322023, 322026, 322029, 322032, 322035, 322047, 322017, 322038, 322041, 322044)),
+ DungeonSpriteAddressData(room_id=223, sprite_id_addresses=(322063, 322066)),
+ DungeonSpriteAddressData(room_id=224, sprite_id_addresses=(322071, 322074, 322077, 322080)),
+ DungeonSpriteAddressData(room_id=232, sprite_id_addresses=(322189, 322192, 322195, 322198)),
+ DungeonSpriteAddressData(room_id=238, sprite_id_addresses=(322213, 322216, 322219, 322222, 322225)),
+ DungeonSpriteAddressData(room_id=239, sprite_id_addresses=(322230, 322233, 322236)),
+ DungeonSpriteAddressData(room_id=249, sprite_id_addresses=(322323, 322326, 322329, 322332)),
+ DungeonSpriteAddressData(room_id=254, sprite_id_addresses=(322378, 322381, 322384, 322387, 322390)),
+ DungeonSpriteAddressData(room_id=263, sprite_id_addresses=(322444, 322447)),
+ DungeonSpriteAddressData(room_id=264, sprite_id_addresses=(322452, 322455, 322458, 322461)),
+ DungeonSpriteAddressData(room_id=267, sprite_id_addresses=(322494,)),
+ DungeonSpriteAddressData(room_id=269, sprite_id_addresses=(322525, 322528)),
+ DungeonSpriteAddressData(room_id=291, sprite_id_addresses=(322671, 322674, 322677, 322680)),
+)
+
+KEYED_SPRITE_ID_ADDRESSES = frozenset((317984,
+ 318044,
+ 318335,
+ 318835,
+ 318915,
+ 318983,
+ 320003,
+ 320011,
+ 320294,
+ 320759,
+ 321292,
+ 321480,
+ 321530,
+ 320000,
+ 321159,
+ 321937,
+ 321940,
+ 321943,
+ 321946,
+ 321949,
+ 321952,
+ 321955,
+ 321958,
+ 321961,
+ 321964,
+ 321967,
+ 321424,
+ 321421,
+ 321418))
diff --git a/worlds/alttp/enemizer_data/enemy_room_metadata.py b/worlds/alttp/enemizer_data/enemy_room_metadata.py
new file mode 100644
index 000000000000..f8d29c153884
--- /dev/null
+++ b/worlds/alttp/enemizer_data/enemy_room_metadata.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+from typing import NamedTuple, Optional
+
+
+class RoomGroupRequirementData(NamedTuple):
+ group_id: Optional[int]
+ subgroup_0: Optional[int]
+ subgroup_1: Optional[int]
+ subgroup_2: Optional[int]
+ subgroup_3: Optional[int]
+ rooms: tuple[int, ...]
+
+SHUTTER_ROOM_IDS = frozenset((184,
+ 11,
+ 27,
+ 75,
+ 4,
+ 36,
+ 182,
+ 40,
+ 14,
+ 46,
+ 62,
+ 110,
+ 49,
+ 135,
+ 68,
+ 69,
+ 83,
+ 117,
+ 133,
+ 61,
+ 93,
+ 107,
+ 109,
+ 123,
+ 125,
+ 141,
+ 150,
+ 165,
+ 113,
+ 168,
+ 216,
+ 176,
+ 192,
+ 224,
+ 178,
+ 210,
+ 239,
+ 268,
+ 291))
+WATER_ROOM_IDS = frozenset((22, 40, 52, 54, 56, 70, 102))
+DONT_RANDOMIZE_ROOM_IDS = frozenset((0, 1, 3, 13, 20, 32, 48, 127))
+NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS = frozenset((1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 85, 96, 97, 98, 112, 113, 114, 128, 129, 130))
+BOSS_ROOM_IDS = frozenset((200, 51, 108, 7, 77, 90, 6, 41, 172, 222, 144, 164, 32, 13, 0))
+
+ROOM_GROUP_REQUIREMENTS = (
+ RoomGroupRequirementData(group_id=1, subgroup_0=70, subgroup_1=73, subgroup_2=28, subgroup_3=82, rooms=(228, 240)),
+ RoomGroupRequirementData(group_id=5, subgroup_0=75, subgroup_1=77, subgroup_2=74, subgroup_3=90, rooms=(243, 265, 270, 271, 272, 273, 282, 284, 290)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=75, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(255, 274, 287)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, rooms=(289,)),
+ RoomGroupRequirementData(group_id=7, subgroup_0=75, subgroup_1=77, subgroup_2=57, subgroup_3=54, rooms=(8, 44, 276, 277)),
+ RoomGroupRequirementData(group_id=13, subgroup_0=81, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(85, 258, 260)),
+ RoomGroupRequirementData(group_id=14, subgroup_0=71, subgroup_1=73, subgroup_2=76, subgroup_3=80, rooms=(18, 261, 266)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=80, rooms=(264,)),
+ RoomGroupRequirementData(group_id=15, subgroup_0=79, subgroup_1=77, subgroup_2=74, subgroup_3=80, rooms=(244, 245, 257, 259, 262, 280, 281)),
+ RoomGroupRequirementData(group_id=18, subgroup_0=85, subgroup_1=61, subgroup_2=66, subgroup_3=67, rooms=(32, 48)),
+ RoomGroupRequirementData(group_id=24, subgroup_0=85, subgroup_1=26, subgroup_2=66, subgroup_3=67, rooms=(13,)),
+ RoomGroupRequirementData(group_id=34, subgroup_0=33, subgroup_1=65, subgroup_2=69, subgroup_3=51, rooms=(0,)),
+ RoomGroupRequirementData(group_id=40, subgroup_0=14, subgroup_1=None, subgroup_2=74, subgroup_3=80, rooms=(225, 256, 293, 292, 294)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=14, subgroup_1=30, subgroup_2=None, subgroup_3=None, rooms=(291,)),
+ RoomGroupRequirementData(group_id=23, subgroup_0=64, subgroup_1=None, subgroup_2=None, subgroup_3=63, rooms=()),
+ RoomGroupRequirementData(group_id=9, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=29, rooms=(227,)),
+ RoomGroupRequirementData(group_id=11, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=61, rooms=()),
+ RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=49, rooms=()),
+ RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=60, subgroup_3=None, rooms=()),
+ RoomGroupRequirementData(group_id=21, subgroup_0=None, subgroup_1=None, subgroup_2=58, subgroup_3=62, rooms=()),
+ RoomGroupRequirementData(group_id=28, subgroup_0=None, subgroup_1=None, subgroup_2=38, subgroup_3=82, rooms=(14, 126, 142, 158, 190)),
+ RoomGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=48, subgroup_3=None, rooms=()),
+ RoomGroupRequirementData(group_id=26, subgroup_0=None, subgroup_1=None, subgroup_2=56, subgroup_3=None, rooms=()),
+ RoomGroupRequirementData(group_id=20, subgroup_0=None, subgroup_1=None, subgroup_2=57, subgroup_3=None, rooms=()),
+ RoomGroupRequirementData(group_id=32, subgroup_0=None, subgroup_1=44, subgroup_2=59, subgroup_3=None, rooms=()),
+ RoomGroupRequirementData(group_id=3, subgroup_0=93, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(81,)),
+ RoomGroupRequirementData(group_id=42, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(286,)),
+ RoomGroupRequirementData(group_id=10, subgroup_0=47, subgroup_1=None, subgroup_2=46, subgroup_3=None, rooms=(92, 117, 185, 217)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(54, 70, 102, 118)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=32, subgroup_2=None, subgroup_3=None, rooms=(62, 159)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=31, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(127,)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=35, subgroup_3=None, rooms=(57, 73, 86, 87, 104, 141)),
+ RoomGroupRequirementData(group_id=37, subgroup_0=31, subgroup_1=None, subgroup_2=39, subgroup_3=82, rooms=(36, 180, 181, 198, 199, 214)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(23, 42, 68, 76, 86, 88, 89, 103, 104, 126, 139, 235, 251)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(23, 42, 76, 89, 103, 104, 126, 139, 235, 251)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(11, 19, 27, 30, 42, 43, 49, 61, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(11, 19, 27, 30, 42, 43, 49, 53, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(19, 35, 150, 165, 195, 197, 213)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(19, 35, 150, 165, 197, 213)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 38, 43, 64, 74, 87, 107, 123)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(38, 43, 64, 74, 87, 107, 123, 206)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(2, 88, 100, 140, 267)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 61, 68, 86, 94, 124, 149, 195)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(4, 63, 206)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(53, 55, 118)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(40,)),
+ RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=37, subgroup_3=None, rooms=(151,)),
+)
diff --git a/worlds/alttp/enemizer_data/enemy_sprite_requirements.py b/worlds/alttp/enemizer_data/enemy_sprite_requirements.py
new file mode 100644
index 000000000000..472cc9d7b1d7
--- /dev/null
+++ b/worlds/alttp/enemizer_data/enemy_sprite_requirements.py
@@ -0,0 +1,295 @@
+from __future__ import annotations
+
+from typing import NamedTuple, Optional
+
+
+class EnemySpriteRequirementData(NamedTuple):
+ sprite_name: str
+ sprite_id: int
+ boss: bool
+ overlord: bool
+ do_not_randomize: bool
+ killable: bool
+ npc: bool
+ never_use_dungeon: bool
+ never_use_overworld: bool
+ cannot_have_key: bool
+ is_object: bool
+ absorbable: bool
+ is_water_sprite: bool
+ is_enemy_sprite: bool
+ group_ids: tuple[int, ...]
+ subgroup_0: tuple[int, ...]
+ subgroup_1: tuple[int, ...]
+ subgroup_2: tuple[int, ...]
+ subgroup_3: tuple[int, ...]
+ parameters: Optional[int]
+ special_glitched: bool
+ excluded_rooms: tuple[int, ...]
+ dont_randomize_rooms: tuple[int, ...]
+ spawnable_rooms: tuple[int, ...]
+
+ENEMY_SPRITE_REQUIREMENTS = (
+ EnemySpriteRequirementData(sprite_name='RavenSprite', sprite_id=0, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17, 25), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='VultureSprite', sprite_id=1, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='EmptySprite', sprite_id=3, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PullSwitch_GoodSprite', sprite_id=4, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PullSwitch_TrapSprite', sprite_id=6, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Octorok_OneWaySprite', sprite_id=8, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MoldormSprite', sprite_id=9, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(48,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Octorok_FourWaySprite', sprite_id=10, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ChickenSprite', sprite_id=11, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21, 80), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BuzzblobSprite', sprite_id=13, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SnapdragonSprite', sprite_id=14, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OctoballoonSprite', sprite_id=15, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OctoballoonHatchlingsSprite', sprite_id=16, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HinoxSprite', sprite_id=17, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MoblinSprite', sprite_id=18, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MiniHelmasaurSprite', sprite_id=19, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GargoylesDomainGateSprite', sprite_id=20, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AntifairySprite', sprite_id=21, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(64, 210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SahasrahlaAginahSprite', sprite_id=22, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BushHoarderSprite', sprite_id=23, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MiniMoldormSprite', sprite_id=24, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PoeSprite', sprite_id=25, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(14, 21), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='DwarvesSprite', sprite_id=26, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArrowInWall_MaybeSprite', sprite_id=27, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='StatueSprite', sprite_id=28, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268, 63), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WeathervaneSprite', sprite_id=29, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CrystalSwitchSprite', sprite_id=30, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BugCatchingKidSprite', sprite_id=31, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(81,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SluggulaSprite', sprite_id=32, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PushSwitchSprite', sprite_id=33, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(83,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RopaSprite', sprite_id=34, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedBariSprite', sprite_id=35, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlueBariSprite', sprite_id=36, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='TalkingTreeSprite', sprite_id=37, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HardhatBeetleSprite', sprite_id=38, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='DeadrockSprite', sprite_id=39, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(127, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='StorytellersSprite', sprite_id=40, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlindHideoutAttendantSprite', sprite_id=41, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14, 79), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SweepingLadySprite', sprite_id=42, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MultipurposeSpriteSprite', sprite_id=43, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LumberjacksSprite', sprite_id=44, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='TelepathicStones_NoIdeaWhatThisActuallyIsLikelyUnusedSprite', sprite_id=45, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FluteBoysNotesSprite', sprite_id=46, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RaceHPNPCsSprite', sprite_id=47, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Person_MaybeSprite', sprite_id=48, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FortuneTellerSprite', sprite_id=49, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AngryBrothersSprite', sprite_id=50, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PullForRupeesSpriteSprite', sprite_id=51, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ScaredGirl2Sprite', sprite_id=52, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='InnkeeperSprite', sprite_id=53, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WitchSprite', sprite_id=54, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WaterfallSprite', sprite_id=55, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArrowTargetSprite', sprite_id=56, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AverageMiddleAgedManSprite', sprite_id=57, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HalfMagicBatSprite', sprite_id=58, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='DashItemSprite', sprite_id=59, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='VillageKidSprite', sprite_id=60, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Signs_ChickenLadyAlsoShowedUp_ScaredLadiesOutsideHousesSprite', sprite_id=61, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RockHoarderSprite', sprite_id=62, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='TutorialSoldierSprite', sprite_id=63, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LightningLockSprite', sprite_id=64, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlueSwordSoldier_DetectPlayerSprite', sprite_id=65, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GreenSwordSoldierSprite', sprite_id=66, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedSpearSoldierSprite', sprite_id=67, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AssaultSwordSoldierSprite', sprite_id=68, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GreenSpearSoldierSprite', sprite_id=69, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlueArcherSprite', sprite_id=70, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GreenArcherSprite', sprite_id=71, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedJavelinSoldierSprite', sprite_id=72, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedJavelinSoldier2Sprite', sprite_id=73, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedBombSoldiersSprite', sprite_id=74, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GreenSoldierRecruits_HMKnightSprite', sprite_id=75, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(19,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GeldmanSprite', sprite_id=76, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RabbitSprite', sprite_id=77, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PopoSprite', sprite_id=78, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Popo2Sprite', sprite_id=79, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CannonBallsSprite', sprite_id=80, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArmosSprite', sprite_id=81, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GiantZoraSprite', sprite_id=82, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArmosKnightsSprite', sprite_id=83, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LanmolasSprite', sprite_id=84, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(49,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FireballZoraSprite', sprite_id=85, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WalkingZoraSprite', sprite_id=86, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='DesertPalaceBarriersSprite', sprite_id=87, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CrabSprite', sprite_id=88, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BirdSprite', sprite_id=89, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SquirrelSprite', sprite_id=90, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Spark_LeftToRightSprite', sprite_id=91, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Spark_RightToLeftSprite', sprite_id=92, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Roller_VerticalMovingSprite', sprite_id=93, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Roller_VerticalMoving2Sprite', sprite_id=94, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RollerSprite', sprite_id=95, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Roller_HorizontalMovingSprite', sprite_id=96, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BeamosSprite', sprite_id=97, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MasterSwordSprite', sprite_id=98, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Devalant_NonShooterSprite', sprite_id=99, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Devalant_ShooterSprite', sprite_id=100, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ShootingGalleryProprietorSprite', sprite_id=101, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_RightSprite', sprite_id=102, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_LeftSprite', sprite_id=103, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_DownSprite', sprite_id=104, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_UpSprite', sprite_id=105, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BallNChainTrooperSprite', sprite_id=106, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CannonSoldierSprite', sprite_id=107, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MirrorPortalSprite', sprite_id=108, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RatSprite', sprite_id=109, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RopeSprite', sprite_id=110, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KeeseSprite', sprite_id=111, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LeeverSprite', sprite_id=113, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ActivatoForThePonds_WhereYouThrowInItemsSprite', sprite_id=114, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='UnclePriestSprite', sprite_id=115, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(71, 81), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RunningManSprite', sprite_id=116, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BottleSalesmanSprite', sprite_id=117, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PrincessZeldaSprite', sprite_id=118, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='VillageElderSprite', sprite_id=120, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AgahnimSprite', sprite_id=122, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(26, 61), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AgahnimEnergyBallSprite', sprite_id=123, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FloatingStalfosHeadSprite', sprite_id=124, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BigSpikeTrapSprite', sprite_id=125, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GuruguruBar_ClockwiseSprite', sprite_id=126, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GuruguruBar_CounterClockwiseSprite', sprite_id=127, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WinderSprite', sprite_id=128, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WaterTektiteSprite', sprite_id=129, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(40,), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AntifairyCircleSprite', sprite_id=130, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GreenEyegoreSprite', sprite_id=131, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedEyegoreSprite', sprite_id=132, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KodongosSprite', sprite_id=134, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MothulaSprite', sprite_id=136, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MothulasBeamSprite', sprite_id=137, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SpikeTrapSprite', sprite_id=138, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(40, 11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GibdoSprite', sprite_id=139, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArrghusSprite', sprite_id=140, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArrghusSpawnSprite', sprite_id=141, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='TerrorpinSprite', sprite_id=142, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SlimeSprite_JumpsOutOfTheFloor', sprite_id=143, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WallmasterSprite', sprite_id=144, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, 51, 67, 83, 99, 115, 116, 117, 131, 132, 133, 7, 23, 39, 49, 119, 135, 167, 32, 48, 64, 176, 192, 208, 224, 9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106, 6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118, 41, 57, 73, 86, 87, 88, 89, 103, 104, 68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220, 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, 222, 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, 155, 156, 157, 165, 166)),
+ EnemySpriteRequirementData(sprite_name='StalfosKnightSprite', sprite_id=145, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HelmasaurKingSprite', sprite_id=146, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(58,), subgroup_3=(62,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BumperSprite', sprite_id=147, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SwimmersEvilSprite', sprite_id=148, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='EyeLaser_RightSprite', sprite_id=149, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='EyeLaser_LeftSprite', sprite_id=150, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='EyeLaser_DownSprite', sprite_id=151, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='EyeLaser_UpSprite', sprite_id=152, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PengatorSprite', sprite_id=153, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KyameronWaterSplashSprite', sprite_id=154, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(40,), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WizzrobeSprite', sprite_id=155, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='VerminHorizontalSprite', sprite_id=156, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='VerminVerticalSprite', sprite_id=157, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Ostrich_HauntedGroveSprite', sprite_id=158, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FluteSprite', sprite_id=159, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Birds_HauntedGroveSprite', sprite_id=160, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FreezorSprite', sprite_id=161, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KholdstareSprite', sprite_id=162, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KholdstaresShellSprite', sprite_id=163, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FallingIceSprite', sprite_id=164, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlueZazakSprite', sprite_id=165, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedZazakSprite', sprite_id=166, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='StalfosSprite', sprite_id=167, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworldSprite', sprite_id=168, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworld2Sprite', sprite_id=169, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='PikitSprite', sprite_id=170, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MaidenSprite', sprite_id=171, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AppleSprite', sprite_id=172, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LostOldManSprite', sprite_id=173, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(28,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='DownPipeSprite', sprite_id=174, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='UpPipeSprite', sprite_id=175, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RightPipeSprite', sprite_id=176, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LeftPipeSprite', sprite_id=177, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GoodBee_AgainMaybeSprite', sprite_id=178, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HylianInscriptionSprite', sprite_id=179, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ThiefsChestSprite', sprite_id=180, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BombSalesmanSprite', sprite_id=181, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KikiSprite', sprite_id=182, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MaidenInBlindDungeonSprite', sprite_id=183, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MimicSprite', sprite_id=184, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FeudingFriendsOnDeathMountainSprite', sprite_id=185, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='WhirlpoolSprite', sprite_id=186, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(255, 274, 287)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(271, 272)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(272,)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(280,)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(293,)),
+ EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(286,)),
+ EnemySpriteRequirementData(sprite_name='DrunkInTheInnSprite', sprite_id=188, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Vitreous_LargeEyeballSprite', sprite_id=189, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Vitreous_SmallEyeballSprite', sprite_id=190, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='VitreousLightningSprite', sprite_id=191, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CatFish_QuakeMedallionSprite', sprite_id=192, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(24,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AgahnimTeleportingZeldaToDarkworldSprite', sprite_id=193, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(61,), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BouldersSprite', sprite_id=194, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='Gibo_FloatingBlobSprite', sprite_id=195, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ThiefSprite', sprite_id=196, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(14, 21), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MedusaSprite', sprite_id=197, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FourWayFireballSpittersSprite', sprite_id=198, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HokkuBokkuSprite', sprite_id=199, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BigFairyWhoHealsYouSprite', sprite_id=200, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='TektiteSprite', sprite_id=201, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ChainChompSprite', sprite_id=202, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='TrinexxSprite', sprite_id=203, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='AnotherPartOfTrinexxSprite', sprite_id=204, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='YetAnotherPartOfTrinexxSprite', sprite_id=205, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlindTheThiefSprite', sprite_id=206, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(59,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SwamolaSprite', sprite_id=207, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LynelSprite', sprite_id=208, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BunnyBeamSprite', sprite_id=209, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FloppingFishSprite', sprite_id=210, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='StalSprite', sprite_id=211, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='LandmineSprite', sprite_id=212, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='DiggingGameProprietorSprite', sprite_id=213, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(42,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GanonSprite', sprite_id=214, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(33,), subgroup_1=(65,), subgroup_2=(69,), subgroup_3=(51,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CopyOfGanon_ExceptInvincibleSprite', sprite_id=215, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HeartSprite', sprite_id=216, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='GreenRupeeSprite', sprite_id=217, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BlueRupeeSprite', sprite_id=218, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='RedRupeeSprite', sprite_id=219, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BombRefill1Sprite', sprite_id=220, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BombRefill4Sprite', sprite_id=221, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BombRefill8Sprite', sprite_id=222, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='SmallMagicRefillSprite', sprite_id=223, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FullMagicRefillSprite', sprite_id=224, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArrowRefill5Sprite', sprite_id=225, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ArrowRefill10Sprite', sprite_id=226, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FairySprite', sprite_id=227, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='KeySprite', sprite_id=228, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BigKeySprite', sprite_id=229, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='ShieldEaterSprite', sprite_id=230, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MushroomSprite', sprite_id=231, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='FakeMasterSwordSprite', sprite_id=232, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MagicShopDude_HisItemsIncludingTheMagicPowderSprite', sprite_id=233, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HeartContainerSprite', sprite_id=234, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='HeartPieceSprite', sprite_id=235, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='BushesSprite', sprite_id=236, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatformSprite', sprite_id=237, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MantleSprite', sprite_id=238, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(93,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused1Sprite', sprite_id=239, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused2Sprite', sprite_id=240, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused3Sprite', sprite_id=241, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='MedallionTabletSprite', sprite_id=242, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OW_OL_FallingRocks', sprite_id=244, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EP4Walls', sprite_id=258, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EPEntrance', sprite_id=259, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_StalfosHeadTrap', sprite_id=261, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_BombDrop_RopeTrap', sprite_id=262, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_MovingFloor', sprite_id=263, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_SlimeDropper', sprite_id=264, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_Wallmaster', sprite_id=265, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Square', sprite_id=266, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Path', sprite_id=267, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_RightEvil_PirogusuSpawner', sprite_id=272, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_LeftEvil_PirogusuSpawner', sprite_id=273, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_DownEvil_PirogusuSpawner', sprite_id=274, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_UpEvil_PirogusuSpawner', sprite_id=275, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_FlyingFloorTileTrap', sprite_id=276, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_WizzrobeSpawner', sprite_id=277, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_BlackSpawn_Zoro_BombHole', sprite_id=278, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_4Skull_Trap_Pot', sprite_id=279, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_Stalfos_Spawn_Trap_EP', sprite_id=280, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_ArmosKnight_Trigger', sprite_id=281, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+ EnemySpriteRequirementData(sprite_name='OL_BombDrop_BombTrap', sprite_id=282, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
+)
diff --git a/worlds/alttp/enemizer_data/overworld_enemy_metadata.py b/worlds/alttp/enemizer_data/overworld_enemy_metadata.py
new file mode 100644
index 000000000000..2c4582ae4ce3
--- /dev/null
+++ b/worlds/alttp/enemizer_data/overworld_enemy_metadata.py
@@ -0,0 +1,131 @@
+from __future__ import annotations
+
+from typing import NamedTuple, Optional
+
+
+class OverworldGroupRequirementData(NamedTuple):
+ group_id: Optional[int]
+ subgroup_0: Optional[int]
+ subgroup_1: Optional[int]
+ subgroup_2: Optional[int]
+ subgroup_3: Optional[int]
+ areas: tuple[int, ...]
+
+AREA_IDS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207)
+DO_NOT_RANDOMIZE_AREA_IDS = frozenset((1,
+ 4,
+ 6,
+ 8,
+ 9,
+ 11,
+ 12,
+ 13,
+ 14,
+ 25,
+ 28,
+ 31,
+ 32,
+ 33,
+ 35,
+ 36,
+ 38,
+ 39,
+ 49,
+ 54,
+ 56,
+ 57,
+ 61,
+ 62,
+ 65,
+ 68,
+ 70,
+ 72,
+ 73,
+ 75,
+ 76,
+ 77,
+ 78,
+ 89,
+ 92,
+ 95,
+ 96,
+ 97,
+ 99,
+ 100,
+ 102,
+ 103,
+ 113,
+ 118,
+ 120,
+ 121,
+ 125,
+ 126,
+ 42,
+ 106,
+ 130,
+ 131,
+ 132,
+ 133,
+ 134,
+ 135,
+ 136,
+ 137,
+ 138,
+ 139,
+ 140,
+ 141,
+ 142,
+ 143,
+ 186,
+ 250,
+ 145,
+ 148,
+ 150,
+ 152,
+ 153,
+ 155,
+ 156,
+ 158,
+ 169,
+ 172,
+ 175,
+ 176,
+ 177,
+ 179,
+ 180,
+ 182,
+ 183,
+ 193,
+ 198,
+ 200,
+ 274,
+ 275,
+ 276,
+ 277,
+ 278,
+ 279,
+ 281,
+ 288))
+
+FORCED_GROUP_REQUIREMENTS = (
+ OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=74, subgroup_3=None, areas=(2,)),
+ OverworldGroupRequirementData(group_id=16, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=16, areas=(3, 147)),
+ OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=17, areas=(10, 154)),
+ OverworldGroupRequirementData(group_id=4, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(15, 159)),
+ OverworldGroupRequirementData(group_id=3, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=14, areas=(20, 164)),
+ OverworldGroupRequirementData(group_id=1, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=63, areas=(27, 171)),
+ OverworldGroupRequirementData(group_id=6, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(34, 40, 178, 184)),
+ OverworldGroupRequirementData(group_id=8, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=None, areas=(48, 192)),
+ OverworldGroupRequirementData(group_id=10, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(58, 202)),
+ OverworldGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=24, subgroup_3=None, areas=(79, 223)),
+ OverworldGroupRequirementData(group_id=21, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=21, areas=(98, 242)),
+ OverworldGroupRequirementData(group_id=27, subgroup_0=None, subgroup_1=42, subgroup_2=None, subgroup_3=None, areas=(104, 248)),
+ OverworldGroupRequirementData(group_id=13, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=None, areas=(22, 166)),
+ OverworldGroupRequirementData(group_id=29, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, areas=(105, 249)),
+ OverworldGroupRequirementData(group_id=15, subgroup_0=None, subgroup_1=None, subgroup_2=78, subgroup_3=None, areas=(42, 186)),
+ OverworldGroupRequirementData(group_id=17, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=76, areas=(106, 250)),
+ OverworldGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=55, subgroup_3=54, areas=(128, 272)),
+ OverworldGroupRequirementData(group_id=14, subgroup_0=None, subgroup_1=None, subgroup_2=12, subgroup_3=68, areas=(129, 273)),
+ OverworldGroupRequirementData(group_id=26, subgroup_0=15, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(146,)),
+ OverworldGroupRequirementData(group_id=23, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=25, areas=(94, 238)),
+)
diff --git a/worlds/alttp/enemizer_data/pot_shuffle_data.py b/worlds/alttp/enemizer_data/pot_shuffle_data.py
new file mode 100644
index 000000000000..5b6ae17c994f
--- /dev/null
+++ b/worlds/alttp/enemizer_data/pot_shuffle_data.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class PotDataRecord(NamedTuple):
+ x: int
+ y: int
+ reserved: int
+
+
+class PotRoomDataRecord(NamedTuple):
+ room_id: int
+ pots: tuple[PotDataRecord, ...]
+ items: tuple[int, ...]
+
+POT_ROOMS = (
+ PotRoomDataRecord(room_id=4, pots=(PotDataRecord(x=162, y=25, reserved=0), PotDataRecord(x=152, y=25, reserved=0), PotDataRecord(x=152, y=22, reserved=0), PotDataRecord(x=162, y=22, reserved=0), PotDataRecord(x=240, y=19, reserved=0), PotDataRecord(x=204, y=19, reserved=0),), items=(10, 10)),
+ PotRoomDataRecord(room_id=9, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(1, 11, 136)),
+ PotRoomDataRecord(room_id=10, pots=(PotDataRecord(x=204, y=11, reserved=0), PotDataRecord(x=156, y=17, reserved=0), PotDataRecord(x=96, y=8, reserved=0), PotDataRecord(x=100, y=7, reserved=0), PotDataRecord(x=160, y=17, reserved=0), PotDataRecord(x=104, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0),), items=(11, 11, 136)),
+ PotRoomDataRecord(room_id=17, pots=(PotDataRecord(x=152, y=19, reserved=0), PotDataRecord(x=152, y=15, reserved=0), PotDataRecord(x=144, y=15, reserved=0), PotDataRecord(x=10, y=15, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=160, y=19, reserved=0),), items=(11, 11, 11, 11)),
+ PotRoomDataRecord(room_id=21, pots=(PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=100, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0), PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=12, y=6, reserved=0), PotDataRecord(x=16, y=6, reserved=0), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=70, y=11, reserved=0),), items=(1, 7, 9, 9, 10, 11, 12, 12, 13)),
+ PotRoomDataRecord(room_id=22, pots=(PotDataRecord(x=188, y=3, reserved=0), PotDataRecord(x=192, y=3, reserved=0), PotDataRecord(x=188, y=4, reserved=0), PotDataRecord(x=192, y=4, reserved=0), PotDataRecord(x=188, y=5, reserved=0), PotDataRecord(x=192, y=5, reserved=0), PotDataRecord(x=188, y=6, reserved=0), PotDataRecord(x=192, y=6, reserved=0), PotDataRecord(x=240, y=19, reserved=0),), items=(8, 9, 9, 10, 10, 11, 11, 12, 12)),
+ PotRoomDataRecord(room_id=26, pots=(PotDataRecord(x=232, y=19, reserved=0), PotDataRecord(x=212, y=19, reserved=0), PotDataRecord(x=28, y=5, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(10, 10, 10, 10)),
+ PotRoomDataRecord(room_id=33, pots=(PotDataRecord(x=100, y=28, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=48, y=28, reserved=0), PotDataRecord(x=82, y=28, reserved=0), PotDataRecord(x=160, y=20, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(11, 12, 12)),
+ PotRoomDataRecord(room_id=35, pots=(PotDataRecord(x=86, y=26, reserved=0), PotDataRecord(x=90, y=26, reserved=0), PotDataRecord(x=94, y=26, reserved=0), PotDataRecord(x=98, y=26, reserved=0), PotDataRecord(x=102, y=26, reserved=0),), items=(1, 10, 11)),
+ PotRoomDataRecord(room_id=36, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(1, 11, 12, 7)),
+ PotRoomDataRecord(room_id=38, pots=(PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=150, y=19, reserved=2), PotDataRecord(x=22, y=26, reserved=2), PotDataRecord(x=220, y=26, reserved=0),), items=(7, 9, 10, 12, 136)),
+ PotRoomDataRecord(room_id=39, pots=(PotDataRecord(x=214, y=19, reserved=0), PotDataRecord(x=214, y=20, reserved=0), PotDataRecord(x=166, y=20, reserved=0), PotDataRecord(x=214, y=21, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=102, y=17, reserved=0), PotDataRecord(x=98, y=17, reserved=0), PotDataRecord(x=106, y=17, reserved=0), PotDataRecord(x=166, y=21, reserved=0), PotDataRecord(x=166, y=19, reserved=0), PotDataRecord(x=92, y=12, reserved=0), PotDataRecord(x=160, y=12, reserved=0),), items=(1, 1, 10, 11, 7, 7)),
+ PotRoomDataRecord(room_id=43, pots=(PotDataRecord(x=16, y=5, reserved=2), PotDataRecord(x=44, y=5, reserved=2), PotDataRecord(x=16, y=6, reserved=2), PotDataRecord(x=44, y=6, reserved=2), PotDataRecord(x=16, y=7, reserved=2), PotDataRecord(x=44, y=7, reserved=2), PotDataRecord(x=146, y=21, reserved=0), PotDataRecord(x=170, y=21, reserved=0), PotDataRecord(x=146, y=22, reserved=0), PotDataRecord(x=170, y=22, reserved=0),), items=(9, 9, 10, 10, 10, 10, 11, 11, 11, 136)),
+ PotRoomDataRecord(room_id=47, pots=(PotDataRecord(x=28, y=7, reserved=0), PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=28, y=9, reserved=0), PotDataRecord(x=32, y=9, reserved=0), PotDataRecord(x=172, y=19, reserved=0), PotDataRecord(x=180, y=19, reserved=0), PotDataRecord(x=104, y=27, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(7, 7, 7, 7, 11, 11, 11, 11)),
+ PotRoomDataRecord(room_id=53, pots=(PotDataRecord(x=60, y=6, reserved=1), PotDataRecord(x=20, y=8, reserved=0), PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=28, y=8, reserved=0), PotDataRecord(x=32, y=8, reserved=0), PotDataRecord(x=36, y=8, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=76, y=23, reserved=1), PotDataRecord(x=88, y=23, reserved=1), PotDataRecord(x=100, y=27, reserved=1), PotDataRecord(x=242, y=28, reserved=1), PotDataRecord(x=240, y=22, reserved=1), PotDataRecord(x=76, y=28, reserved=1),), items=(7, 7, 7, 7, 7, 8, 11)),
+ PotRoomDataRecord(room_id=54, pots=(PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=10, y=16, reserved=0), PotDataRecord(x=114, y=16, reserved=0),), items=(8, 10, 11)),
+ PotRoomDataRecord(room_id=55, pots=(PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=60, y=6, reserved=0),), items=(8,)),
+ PotRoomDataRecord(room_id=56, pots=(PotDataRecord(x=164, y=12, reserved=0), PotDataRecord(x=164, y=13, reserved=0), PotDataRecord(x=164, y=18, reserved=0), PotDataRecord(x=164, y=19, reserved=0),), items=(8, 7, 10, 10)),
+ PotRoomDataRecord(room_id=57, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=100, y=22, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 9, 11, 12)),
+ PotRoomDataRecord(room_id=60, pots=(PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=64, y=12, reserved=0), PotDataRecord(x=20, y=14, reserved=0), PotDataRecord(x=68, y=18, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=64, y=20, reserved=0), PotDataRecord(x=64, y=26, reserved=0),), items=(1, 7, 7, 7, 7, 11, 12)),
+ PotRoomDataRecord(room_id=61, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=24, y=22, reserved=0), PotDataRecord(x=40, y=22, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0),), items=(9, 7, 10, 10, 11, 11, 13)),
+ PotRoomDataRecord(room_id=62, pots=(PotDataRecord(x=96, y=6, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=92, y=10, reserved=0),), items=(10, 11, 12, 12)),
+ PotRoomDataRecord(room_id=63, pots=(PotDataRecord(x=12, y=25, reserved=0), PotDataRecord(x=20, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=12, y=27, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=28, y=23, reserved=0),), items=(1, 1, 8, 10, 10, 11, 136)),
+ PotRoomDataRecord(room_id=65, pots=(PotDataRecord(x=100, y=10, reserved=0), PotDataRecord(x=52, y=15, reserved=0), PotDataRecord(x=52, y=16, reserved=0), PotDataRecord(x=148, y=22, reserved=0),), items=(1, 11, 12, 12)),
+ PotRoomDataRecord(room_id=67, pots=(PotDataRecord(x=112, y=28, reserved=1), PotDataRecord(x=76, y=28, reserved=1), PotDataRecord(x=76, y=20, reserved=1), PotDataRecord(x=66, y=4, reserved=0), PotDataRecord(x=78, y=4, reserved=0), PotDataRecord(x=66, y=9, reserved=0), PotDataRecord(x=78, y=9, reserved=0), PotDataRecord(x=112, y=20, reserved=1),), items=(8, 9, 11, 11, 12)),
+ PotRoomDataRecord(room_id=69, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=108, y=11, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=220, y=16, reserved=0), PotDataRecord(x=236, y=16, reserved=0),), items=(9, 9, 11, 11, 12)),
+ PotRoomDataRecord(room_id=73, pots=(PotDataRecord(x=156, y=27, reserved=0), PotDataRecord(x=172, y=24, reserved=0), PotDataRecord(x=172, y=23, reserved=0), PotDataRecord(x=144, y=20, reserved=0), PotDataRecord(x=104, y=15, reserved=0), PotDataRecord(x=104, y=16, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=172, y=20, reserved=0), PotDataRecord(x=144, y=27, reserved=0), PotDataRecord(x=172, y=28, reserved=0), PotDataRecord(x=160, y=27, reserved=0),), items=(11, 11, 12, 12, 12, 12)),
+ PotRoomDataRecord(room_id=78, pots=(PotDataRecord(x=48, y=10, reserved=2), PotDataRecord(x=140, y=11, reserved=2), PotDataRecord(x=28, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=0),), items=(136, 11, 12)),
+ PotRoomDataRecord(room_id=83, pots=(PotDataRecord(x=92, y=11, reserved=0), PotDataRecord(x=96, y=11, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=11, reserved=0),), items=(8, 11, 11, 12)),
+ PotRoomDataRecord(room_id=84, pots=(PotDataRecord(x=186, y=25, reserved=0), PotDataRecord(x=186, y=26, reserved=0), PotDataRecord(x=186, y=27, reserved=0), PotDataRecord(x=186, y=28, reserved=0),), items=(7, 11, 11, 11)),
+ PotRoomDataRecord(room_id=86, pots=(PotDataRecord(x=100, y=6, reserved=1), PotDataRecord(x=96, y=10, reserved=1), PotDataRecord(x=92, y=10, reserved=1), PotDataRecord(x=48, y=20, reserved=1), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=40, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=36, y=7, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=24, y=9, reserved=0), PotDataRecord(x=36, y=9, reserved=0), PotDataRecord(x=20, y=10, reserved=0), PotDataRecord(x=40, y=10, reserved=0), PotDataRecord(x=12, y=20, reserved=1),), items=(7, 7, 11, 11, 8, 12, 12, 12, 12, 12, 12)),
+ PotRoomDataRecord(room_id=87, pots=(PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=12, y=20, reserved=2), PotDataRecord(x=92, y=23, reserved=0), PotDataRecord(x=100, y=23, reserved=0), PotDataRecord(x=84, y=25, reserved=0), PotDataRecord(x=76, y=27, reserved=0), PotDataRecord(x=48, y=20, reserved=2), PotDataRecord(x=30, y=22, reserved=2),), items=(7, 10, 11, 12, 12, 12, 13, 136)),
+ PotRoomDataRecord(room_id=88, pots=(PotDataRecord(x=96, y=9, reserved=0), PotDataRecord(x=92, y=8, reserved=0), PotDataRecord(x=108, y=8, reserved=0), PotDataRecord(x=108, y=6, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=92, y=6, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=16, y=7, reserved=0), PotDataRecord(x=96, y=5, reserved=0), PotDataRecord(x=100, y=5, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=108, y=7, reserved=0), PotDataRecord(x=16, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0), PotDataRecord(x=104, y=9, reserved=0),), items=(10, 10, 11, 11, 12, 12, 12, 12)),
+ PotRoomDataRecord(room_id=91, pots=(PotDataRecord(x=218, y=37, reserved=0), PotDataRecord(x=222, y=37, reserved=0), PotDataRecord(x=226, y=37, reserved=0),), items=(136,)),
+ PotRoomDataRecord(room_id=92, pots=(PotDataRecord(x=228, y=25, reserved=0), PotDataRecord(x=104, y=24, reserved=0), PotDataRecord(x=228, y=22, reserved=0), PotDataRecord(x=216, y=25, reserved=0), PotDataRecord(x=84, y=24, reserved=0), PotDataRecord(x=216, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=94, y=26, reserved=0),), items=(10, 13)),
+ PotRoomDataRecord(room_id=93, pots=(PotDataRecord(x=16, y=5, reserved=0), PotDataRecord(x=44, y=5, reserved=0), PotDataRecord(x=16, y=11, reserved=0), PotDataRecord(x=44, y=11, reserved=0), PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(1, 7, 9, 9, 10, 10, 10, 12)),
+ PotRoomDataRecord(room_id=94, pots=(PotDataRecord(x=92, y=4, reserved=0), PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=112, y=8, reserved=0),), items=(11, 11, 12, 12)),
+ PotRoomDataRecord(room_id=99, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(8, 11)),
+ PotRoomDataRecord(room_id=100, pots=(PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=16, y=22, reserved=0), PotDataRecord(x=20, y=22, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(10, 10, 10, 10, 12, 12, 136)),
+ PotRoomDataRecord(room_id=102, pots=(PotDataRecord(x=48, y=37, reserved=0), PotDataRecord(x=52, y=37, reserved=0), PotDataRecord(x=56, y=37, reserved=0), PotDataRecord(x=84, y=5, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=48, y=38, reserved=0), PotDataRecord(x=52, y=38, reserved=0), PotDataRecord(x=56, y=38, reserved=0), PotDataRecord(x=84, y=6, reserved=0), PotDataRecord(x=104, y=6, reserved=0),), items=(7, 7, 9, 9, 9, 10, 10, 10, 11, 11)),
+ PotRoomDataRecord(room_id=103, pots=(PotDataRecord(x=22, y=26, reserved=0), PotDataRecord(x=18, y=22, reserved=0), PotDataRecord(x=92, y=9, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=48, y=7, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=74, y=20, reserved=0), PotDataRecord(x=18, y=23, reserved=0), PotDataRecord(x=18, y=26, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(9, 11, 11, 11, 12, 12, 12)),
+ PotRoomDataRecord(room_id=104, pots=(PotDataRecord(x=84, y=14, reserved=0), PotDataRecord(x=84, y=13, reserved=0), PotDataRecord(x=88, y=12, reserved=0), PotDataRecord(x=88, y=6, reserved=0), PotDataRecord(x=88, y=5, reserved=0), PotDataRecord(x=88, y=4, reserved=0), PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=64, y=15, reserved=0), PotDataRecord(x=64, y=7, reserved=0), PotDataRecord(x=88, y=7, reserved=0), PotDataRecord(x=64, y=16, reserved=0), PotDataRecord(x=64, y=24, reserved=0), PotDataRecord(x=64, y=25, reserved=0),), items=(11, 11, 11, 12, 12)),
+ PotRoomDataRecord(room_id=115, pots=(PotDataRecord(x=154, y=21, reserved=0), PotDataRecord(x=158, y=21, reserved=0), PotDataRecord(x=20, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=144, y=24, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0), PotDataRecord(x=154, y=27, reserved=0), PotDataRecord(x=158, y=27, reserved=0),), items=(1, 1, 11, 11, 7, 7, 9, 9, 12, 136)),
+ PotRoomDataRecord(room_id=116, pots=(PotDataRecord(x=30, y=5, reserved=0), PotDataRecord(x=62, y=5, reserved=0), PotDataRecord(x=94, y=5, reserved=0), PotDataRecord(x=14, y=11, reserved=0), PotDataRecord(x=46, y=11, reserved=0), PotDataRecord(x=78, y=11, reserved=0), PotDataRecord(x=110, y=11, reserved=0),), items=(9, 9, 11, 11, 12, 12, 136)),
+ PotRoomDataRecord(room_id=117, pots=(PotDataRecord(x=148, y=22, reserved=0), PotDataRecord(x=160, y=22, reserved=0), PotDataRecord(x=172, y=22, reserved=0),), items=(9, 11, 12)),
+ PotRoomDataRecord(room_id=123, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=76, y=7, reserved=0), PotDataRecord(x=60, y=4, reserved=0), PotDataRecord(x=64, y=4, reserved=0),), items=(11, 8)),
+ PotRoomDataRecord(room_id=124, pots=(PotDataRecord(x=36, y=21, reserved=0), PotDataRecord(x=24, y=11, reserved=0), PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=32, y=4, reserved=0),), items=(11, 11)),
+ PotRoomDataRecord(room_id=125, pots=(PotDataRecord(x=44, y=12, reserved=0), PotDataRecord(x=44, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=108, y=20, reserved=0), PotDataRecord(x=114, y=20, reserved=0), PotDataRecord(x=76, y=28, reserved=0),), items=(9, 10, 10, 11)),
+ PotRoomDataRecord(room_id=126, pots=(PotDataRecord(x=86, y=15, reserved=0), PotDataRecord(x=82, y=26, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=104, y=26, reserved=0),), items=(11, 12, 136)),
+ PotRoomDataRecord(room_id=130, pots=(PotDataRecord(x=50, y=5, reserved=0), PotDataRecord(x=50, y=10, reserved=0), PotDataRecord(x=76, y=50, reserved=0),), items=(11,)),
+ PotRoomDataRecord(room_id=131, pots=(PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=80, y=4, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0),), items=(1, 7, 9, 9)),
+ PotRoomDataRecord(room_id=132, pots=(PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=60, y=17, reserved=0), PotDataRecord(x=80, y=14, reserved=0), PotDataRecord(x=44, y=14, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=24, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=100, y=7, reserved=0),), items=(9, 9)),
+ PotRoomDataRecord(room_id=135, pots=(PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=112, y=20, reserved=0), PotDataRecord(x=16, y=12, reserved=0), PotDataRecord(x=40, y=12, reserved=0), PotDataRecord(x=32, y=12, reserved=0), PotDataRecord(x=24, y=12, reserved=0), PotDataRecord(x=16, y=11, reserved=0),), items=(12, 13)),
+ PotRoomDataRecord(room_id=139, pots=(PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=76, y=12, reserved=1), PotDataRecord(x=32, y=23, reserved=1), PotDataRecord(x=28, y=23, reserved=1), PotDataRecord(x=112, y=12, reserved=1), PotDataRecord(x=32, y=9, reserved=1), PotDataRecord(x=76, y=28, reserved=0),), items=(8, 11, 12)),
+ PotRoomDataRecord(room_id=140, pots=(PotDataRecord(x=76, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=2), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=92, y=20, reserved=0), PotDataRecord(x=100, y=21, reserved=0), PotDataRecord(x=104, y=26, reserved=0), PotDataRecord(x=88, y=27, reserved=0),), items=(9, 10, 10, 10, 10, 12, 136)),
+ PotRoomDataRecord(room_id=145, pots=(PotDataRecord(x=84, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0),), items=(11, 12)),
+ PotRoomDataRecord(room_id=150, pots=(PotDataRecord(x=14, y=18, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=32, y=17, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=76, y=21, reserved=0), PotDataRecord(x=112, y=21, reserved=0), PotDataRecord(x=14, y=24, reserved=0),), items=(11, 12, 12, 13)),
+ PotRoomDataRecord(room_id=155, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(8, 12)),
+ PotRoomDataRecord(room_id=157, pots=(PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=40, y=9, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=84, y=4, reserved=0),), items=(10, 12)),
+ PotRoomDataRecord(room_id=159, pots=(PotDataRecord(x=138, y=20, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=138, y=21, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=138, y=27, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=21, reserved=0), PotDataRecord(x=178, y=20, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=178, y=27, reserved=0), PotDataRecord(x=178, y=26, reserved=0), PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=138, y=26, reserved=0), PotDataRecord(x=20, y=21, reserved=0),), items=(8, 11, 11, 11, 11, 11, 136)),
+ PotRoomDataRecord(room_id=161, pots=(PotDataRecord(x=96, y=27, reserved=0), PotDataRecord(x=92, y=21, reserved=0), PotDataRecord(x=150, y=6, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=12, reserved=0), PotDataRecord(x=108, y=13, reserved=0), PotDataRecord(x=112, y=14, reserved=0), PotDataRecord(x=96, y=23, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(8, 11, 11, 11, 12, 12)),
+ PotRoomDataRecord(room_id=168, pots=(PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=30, y=24, reserved=0),), items=(1, 11)),
+ PotRoomDataRecord(room_id=169, pots=(PotDataRecord(x=12, y=19, reserved=0), PotDataRecord(x=112, y=19, reserved=0), PotDataRecord(x=144, y=43, reserved=0), PotDataRecord(x=236, y=43, reserved=0), PotDataRecord(x=144, y=44, reserved=0), PotDataRecord(x=236, y=44, reserved=0), PotDataRecord(x=16, y=20, reserved=0), PotDataRecord(x=108, y=20, reserved=0),), items=(11, 11, 11, 9, 9, 9)),
+ PotRoomDataRecord(room_id=170, pots=(PotDataRecord(x=212, y=10, reserved=2), PotDataRecord(x=232, y=10, reserved=2), PotDataRecord(x=232, y=5, reserved=2), PotDataRecord(x=212, y=5, reserved=2), PotDataRecord(x=94, y=8, reserved=2), PotDataRecord(x=108, y=55, reserved=0), PotDataRecord(x=108, y=56, reserved=0), PotDataRecord(x=108, y=57, reserved=0),), items=(11, 11, 11, 11, 136)),
+ PotRoomDataRecord(room_id=176, pots=(PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=24, y=24, reserved=0), PotDataRecord(x=44, y=25, reserved=0), PotDataRecord(x=20, y=21, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=16, y=23, reserved=0), PotDataRecord(x=44, y=23, reserved=0), PotDataRecord(x=36, y=24, reserved=0), PotDataRecord(x=16, y=25, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(1, 1, 7, 7, 9, 9, 10, 10, 11, 11)),
+ PotRoomDataRecord(room_id=179, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(8, 12, 136)),
+ PotRoomDataRecord(room_id=180, pots=(PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(11, 13)),
+ PotRoomDataRecord(room_id=181, pots=(PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=112, y=15, reserved=0), PotDataRecord(x=76, y=16, reserved=0), PotDataRecord(x=112, y=16, reserved=0), PotDataRecord(x=112, y=17, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 10, 11, 11, 13, 136)),
+ PotRoomDataRecord(room_id=184, pots=(PotDataRecord(x=96, y=13, reserved=0), PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=104, y=16, reserved=0),), items=(11, 11, 136)),
+ PotRoomDataRecord(room_id=185, pots=(PotDataRecord(x=92, y=18, reserved=0), PotDataRecord(x=96, y=18, reserved=0), PotDataRecord(x=104, y=18, reserved=0), PotDataRecord(x=108, y=18, reserved=0),), items=(1, 1, 7, 7)),
+ PotRoomDataRecord(room_id=186, pots=(PotDataRecord(x=100, y=8, reserved=0), PotDataRecord(x=88, y=8, reserved=0), PotDataRecord(x=94, y=4, reserved=0), PotDataRecord(x=76, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=76, y=10, reserved=0), PotDataRecord(x=112, y=10, reserved=0), PotDataRecord(x=94, y=12, reserved=0),), items=(1, 1, 8, 11, 11, 12)),
+ PotRoomDataRecord(room_id=188, pots=(PotDataRecord(x=138, y=3, reserved=2), PotDataRecord(x=178, y=3, reserved=2), PotDataRecord(x=86, y=4, reserved=1), PotDataRecord(x=102, y=4, reserved=1), PotDataRecord(x=138, y=12, reserved=2), PotDataRecord(x=178, y=12, reserved=2), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(7, 7, 7, 7, 8, 10, 10, 10, 10, 10, 11, 11, 136)),
+ PotRoomDataRecord(room_id=191, pots=(PotDataRecord(x=40, y=20, reserved=0), PotDataRecord(x=44, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 10, 11, 12, 12, 12)),
+ PotRoomDataRecord(room_id=192, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=12, y=14, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=28, y=27, reserved=0),), items=(1, 7, 10, 11)),
+ PotRoomDataRecord(room_id=194, pots=(PotDataRecord(x=180, y=7, reserved=0), PotDataRecord(x=100, y=46, reserved=0), PotDataRecord(x=68, y=48, reserved=0), PotDataRecord(x=64, y=52, reserved=0),), items=(1, 9, 12, 136)),
+ PotRoomDataRecord(room_id=196, pots=(PotDataRecord(x=84, y=9, reserved=0), PotDataRecord(x=24, y=14, reserved=0), PotDataRecord(x=56, y=17, reserved=0), PotDataRecord(x=84, y=17, reserved=0), PotDataRecord(x=12, y=21, reserved=0), PotDataRecord(x=76, y=23, reserved=0), PotDataRecord(x=48, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0),), items=(1, 9, 12, 7, 10, 10, 11, 11)),
+ PotRoomDataRecord(room_id=199, pots=(PotDataRecord(x=12, y=10, reserved=0), PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(9, 12, 11, 13)),
+ PotRoomDataRecord(room_id=201, pots=(PotDataRecord(x=30, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=60, y=22, reserved=0),), items=(1, 1, 136)),
+ PotRoomDataRecord(room_id=203, pots=(PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=88, y=28, reserved=0),), items=(7, 11)),
+ PotRoomDataRecord(room_id=204, pots=(PotDataRecord(x=36, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 11, 7, 10)),
+ PotRoomDataRecord(room_id=206, pots=(PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=80, y=8, reserved=0), PotDataRecord(x=108, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=204, y=11, reserved=3),), items=(9, 12, 12, 10, 128)),
+ PotRoomDataRecord(room_id=208, pots=(PotDataRecord(x=158, y=5, reserved=0), PotDataRecord(x=140, y=11, reserved=0), PotDataRecord(x=42, y=13, reserved=0), PotDataRecord(x=48, y=16, reserved=0), PotDataRecord(x=176, y=20, reserved=0), PotDataRecord(x=146, y=23, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(1, 1, 7, 11, 11, 12, 12)),
+ PotRoomDataRecord(room_id=209, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=168, y=7, reserved=0), PotDataRecord(x=112, y=12, reserved=0),), items=(9, 1, 1, 1, 13)),
+ PotRoomDataRecord(room_id=214, pots=(PotDataRecord(x=92, y=22, reserved=0), PotDataRecord(x=96, y=22, reserved=0),), items=(10, 13)),
+ PotRoomDataRecord(room_id=216, pots=(PotDataRecord(x=202, y=8, reserved=0), PotDataRecord(x=242, y=8, reserved=0), PotDataRecord(x=202, y=10, reserved=0), PotDataRecord(x=242, y=10, reserved=0), PotDataRecord(x=202, y=12, reserved=0), PotDataRecord(x=242, y=12, reserved=0), PotDataRecord(x=92, y=24, reserved=0), PotDataRecord(x=96, y=24, reserved=0),), items=(9, 9, 9, 9, 9, 11, 11, 11)),
+ PotRoomDataRecord(room_id=218, pots=(PotDataRecord(x=24, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=24, y=25, reserved=0), PotDataRecord(x=36, y=25, reserved=0),), items=(9, 9, 11, 136)),
+ PotRoomDataRecord(room_id=219, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=62, y=19, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=88, y=16, reserved=0),), items=(7, 11)),
+ PotRoomDataRecord(room_id=220, pots=(PotDataRecord(x=56, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=68, y=16, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(7, 9, 10, 11)),
+ PotRoomDataRecord(room_id=235, pots=(PotDataRecord(x=206, y=8, reserved=0), PotDataRecord(x=210, y=8, reserved=0), PotDataRecord(x=88, y=14, reserved=0), PotDataRecord(x=92, y=14, reserved=0), PotDataRecord(x=96, y=14, reserved=0),), items=(7, 7, 11, 12, 12)),
+)
diff --git a/worlds/alttp/enemizer_data/symbols.py b/worlds/alttp/enemizer_data/symbols.py
new file mode 100644
index 000000000000..903d2ae7d5ed
--- /dev/null
+++ b/worlds/alttp/enemizer_data/symbols.py
@@ -0,0 +1,171 @@
+from __future__ import annotations
+
+ENEMIZER_SYMBOLS = {
+ ':pos_1_0': 0x36975E,
+ ':pos_1_1': 0x36976D,
+ ':pos_1_10': 0x369819,
+ ':pos_1_11': 0x369828,
+ ':pos_1_12': 0x369837,
+ ':pos_1_13': 0x369845,
+ ':pos_1_14': 0x36984B,
+ ':pos_1_15': 0x369851,
+ ':pos_1_16': 0x369873,
+ ':pos_1_17': 0x369898,
+ ':pos_1_18': 0x36989E,
+ ':pos_1_19': 0x3698A4,
+ ':pos_1_2': 0x369787,
+ ':pos_1_20': 0x3698C6,
+ ':pos_1_21': 0x3698EB,
+ ':pos_1_22': 0x3698F1,
+ ':pos_1_23': 0x3698F7,
+ ':pos_1_24': 0x369919,
+ ':pos_1_25': 0x36993E,
+ ':pos_1_26': 0x369944,
+ ':pos_1_27': 0x36994A,
+ ':pos_1_28': 0x36996C,
+ ':pos_1_29': 0x369AA5,
+ ':pos_1_3': 0x369796,
+ ':pos_1_30': 0x36B796,
+ ':pos_1_4': 0x3697A5,
+ ':pos_1_5': 0x3697B4,
+ ':pos_1_6': 0x3697D5,
+ ':pos_1_7': 0x3697E8,
+ ':pos_1_8': 0x3697F7,
+ ':pos_1_9': 0x369806,
+ 'CheckIfLinkShouldDie': 0x3699B2,
+ 'CheckIfLinkShouldDie_dead': 0x3699BB,
+ 'CheckIfLinkShouldDie_done': 0x3699BD,
+ 'Check_for_Blind_Fight': 0x1DA085,
+ 'CopyShield': 0x36B713,
+ 'CopyShield_loop_copy': 0x36B729,
+ 'CopyShield_shield_positon_gfx': 0x36B74B,
+ 'CopySword': 0x36B6D1,
+ 'CopySword_loop_copy': 0x36B6E7,
+ 'CopySword_sword_positon_gfx': 0x36B709,
+ 'DMAKholdstare': 0x3695A5,
+ 'DMATrinexx': 0x369617,
+ 'Dungeon_ResetSprites': 0x09C114,
+ 'EnemizerCodeStart': 0x3694F5,
+ 'EnemizerFlags': 0x368100,
+ 'EnemizerFlags_agahnim_fun_balls': 0x368104,
+ 'EnemizerFlags_close_blind_door': 0x368101,
+ 'EnemizerFlags_enable_mimic_override': 0x368105,
+ 'EnemizerFlags_enable_terrorpin_ai_fix': 0x368106,
+ 'EnemizerFlags_moldorm_eye_count': 0x368102,
+ 'EnemizerFlags_randomize_bushes': 0x368100,
+ 'EnemizerFlags_randomize_sprites': 0x368103,
+ 'EnemizerTablesStart': 0x368000,
+ 'Ext_OnBossDeath': 0x29803C,
+ 'Ext_OnDungeonCompleted': 0x29804B,
+ 'Ext_OnDungeonEnter': 0x298041,
+ 'Ext_OnDungeonExit': 0x298046,
+ 'Ext_OnFairyRevive': 0x298019,
+ 'Ext_OnFileCreate': 0x298000,
+ 'Ext_OnFileLoad': 0x298005,
+ 'Ext_OnFileSave': 0x29800A,
+ 'Ext_OnIemMenuOpen': 0x298023,
+ 'Ext_OnItemChange': 0x29802D,
+ 'Ext_OnItemMenuClose': 0x298028,
+ 'Ext_OnMapUse': 0x298014,
+ 'Ext_OnPlayerAttack': 0x298037,
+ 'Ext_OnPlayerDamaged': 0x298032,
+ 'Ext_OnPlayerDeath': 0x29800F,
+ 'Ext_OnYItemUse': 0x29801E,
+ 'Ext_OnZeldaRescued': 0x298050,
+ 'FixTerrorpin': 0x36971A,
+ 'FixTerrorpin_new': 0x369728,
+ 'GFX_Kholdstare_Shell': 0x36C79A,
+ 'GFX_Trinexx_Shell': 0x36D79A,
+ 'GFX_Trinexx_Shell2': 0x36DF9A,
+ 'GetRandomInt': 0x0DBA71,
+ 'Initialize_Blind_Fight': 0x1DA090,
+ 'Kholdstare_Draw': 0x0DD97F,
+ 'LoadFile': 0x369A87,
+ 'LoadNewSoundFx': 0x369A9E,
+ 'LoadOverworldSprites': 0x36B753,
+ 'Module_LoadFile_indoors': 0x028118,
+ 'Moldorm_UpdateOamPosition': 0x3699E7,
+ 'Moldorm_UpdateOamPosition_more_eyes': 0x3699ED,
+ 'NMIHookAction': 0x36956C,
+ 'NMIHookAction_loadKholdstare': 0x369584,
+ 'NMIHookAction_loadTrinexx': 0x369590,
+ 'NMIHookAction_return': 0x36959A,
+ 'NMIHookReturn': 0x0080D5,
+ 'NewLoadSoundBank': 0x369A98,
+ 'NewLoadSoundBank_Intro': 0x369A92,
+ 'OnInitFileSelect': 0x3696FA,
+ 'OnInitFileSelect_continue': 0x36970F,
+ 'Player_Main': 0x078000,
+ 'Sound_LoadSongBank': 0x008888,
+ 'Sound_SetSfx3PanLong': 0x0DBB8A,
+ 'Sound_SetSfxPanWithPlayerCoords': 0x0DBB67,
+ 'Spawn_Bees': 0x36B75D,
+ 'Spawn_Bees_done': 0x36B779,
+ 'SpritePrep_Eyegore': 0x1EC6FA,
+ 'SpritePrep_EyegoreNew': 0x369A1A,
+ 'SpritePrep_EyegoreNew_mimic': 0x369A31,
+ 'SpritePrep_EyegoreNew_new': 0x369A25,
+ 'Sprite_ResetAll': 0x09C44E,
+ 'Sprite_SpawnDynamically': 0x1DF65D,
+ 'VitreousKeyReset': 0x36B77A,
+ 'boss_move': 0x369743,
+ 'boss_move_loop_bottom_left': 0x369935,
+ 'boss_move_loop_bottom_left2': 0x369963,
+ 'boss_move_loop_bottom_right': 0x3698E2,
+ 'boss_move_loop_bottom_right2': 0x369910,
+ 'boss_move_loop_middle': 0x36983C,
+ 'boss_move_loop_middle2': 0x36986A,
+ 'boss_move_loop_top_right': 0x36988F,
+ 'boss_move_loop_top_right2': 0x3698BD,
+ 'boss_move_move_to_bottom_left': 0x369933,
+ 'boss_move_move_to_bottom_right': 0x3698E0,
+ 'boss_move_move_to_middle': 0x36983A,
+ 'boss_move_move_to_top_right': 0x36988D,
+ 'boss_move_no_blind_door': 0x3697D2,
+ 'boss_move_no_change': 0x369863,
+ 'boss_move_no_change2': 0x3698B6,
+ 'boss_move_no_change3': 0x369909,
+ 'boss_move_no_change4': 0x36995C,
+ 'boss_move_no_change_ov': 0x369885,
+ 'boss_move_no_change_ov2': 0x3698D8,
+ 'boss_move_no_change_ov3': 0x36992B,
+ 'boss_move_no_change_ov4': 0x36997E,
+ 'boss_move_return': 0x369986,
+ 'change_heartcontainer_position': 0x3699BE,
+ 'change_heartcontainer_position_not_moldorm_room': 0x3699E1,
+ 'check_blind_boss_room': 0x36B782,
+ 'check_special_action': 0x369731,
+ 'check_special_action_no_special_action': 0x36973E,
+ 'enemizer_info_table': 0x368000,
+ 'linkIsDead': 0x0780D5,
+ 'linkNotDead': 0x0780F7,
+ 'modified_room_object_table': 0x36B79A,
+ 'moved_room_header_bank_value_address': 0x368374,
+ 'newKodongoCollision': 0x369A02,
+ 'newKodongoCollision_continue': 0x369A19,
+ 'new_kholdstare_code': 0x369987,
+ 'new_kholdstare_code_already_iced': 0x369997,
+ 'new_trinexx_code': 0x36999C,
+ 'new_trinexx_code_already_rocked': 0x3699AC,
+ 'notItemSprite_Mimic': 0x369A66,
+ 'notItemSprite_Mimic_changeSpriteId': 0x369A7A,
+ 'notItemSprite_Mimic_continue': 0x369A82,
+ 'notItemSprite_Mimic_reloadSpriteIdAndSkipMimic': 0x369A7F,
+ 'resetSprite_Mimic': 0x369A4E,
+ 'resetSprite_Mimic_notMimic': 0x369A60,
+ 'room_header_table': 0x368375,
+ 'shieldgfx': 0x36AAD1,
+ 'sprite_bush_spawn': 0x3694F5,
+ 'sprite_bush_spawn_continue': 0x36950F,
+ 'sprite_bush_spawn_dontGoPhase2': 0x369565,
+ 'sprite_bush_spawn_item_table': 0x369525,
+ 'sprite_bush_spawn_newSpriteSpawn': 0x36954C,
+ 'sprite_bush_spawn_not_random': 0x36953B,
+ 'sprite_bush_spawn_not_random_old': 0x36950B,
+ 'sprite_bush_spawn_return': 0x369568,
+ 'sprite_bush_spawn_table': 0x368120,
+ 'sprite_bush_spawn_table_dungeons': 0x368248,
+ 'sprite_bush_spawn_table_overworld': 0x368120,
+ 'sprite_bush_spawn_table_random_sprites': 0x368370,
+ 'swordgfx': 0x369AD1,
+}
diff --git a/worlds/alttp/test/TestEnemizerPatches.py b/worlds/alttp/test/TestEnemizerPatches.py
new file mode 100644
index 000000000000..4aa09bb62e2a
--- /dev/null
+++ b/worlds/alttp/test/TestEnemizerPatches.py
@@ -0,0 +1,308 @@
+import unittest
+from types import SimpleNamespace
+
+from worlds.alttp.EnemizerPatches import (
+ ARROW_REFILL_5_SPRITE_ID,
+ BOSS_GFX_SHEET_INDEXES,
+ BOSS_PATCH_DATA,
+ DAMAGE_GROUP_TABLE_ADDRESS,
+ DUNGEON_BOSS_PATCH_DATA,
+ ENEMY_DAMAGE_TABLE_ADDRESS,
+ ENEMY_HP_TABLE_ADDRESS,
+ EXCLUDED_ENEMY_TABLE_SPRITE_IDS,
+ HIDDEN_ENEMY_CHANCE_POOL_ADDRESS,
+ RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
+ RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS,
+ RETRO_RUPEE_REPLACEMENT_SPRITE_ID,
+ THIEF_DEFAULT_HP,
+ THIEF_SPRITE_ID,
+ TILE_TRAP_FLOOR_TILE_ADDRESS,
+ TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS,
+ TRINEXX_ICE_PROJECTILE_TILE_ADDRESS,
+ VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
+ _apply_killable_thief,
+ _apply_randomized_tile_trap_floor_tile,
+ _get_enemizer_symbol,
+ _make_native_enemizer_rng,
+ _option_key,
+ patch_bosses,
+ _randomize_enemy_damage,
+ _randomize_enemy_health,
+ _set_enemizer_flag,
+ _shuffle_damage_groups,
+ _update_hidden_enemy_item_table_for_retro_mode,
+ apply_enemizer_base_patch,
+)
+
+
+class FakeRom:
+ def __init__(self, size: int = 0x400000) -> None:
+ self.buffer = bytearray(size)
+
+ def read_byte(self, address: int) -> int:
+ return self.buffer[address]
+
+ def read_bytes(self, startaddress: int, length: int) -> bytearray:
+ return self.buffer[startaddress:startaddress + length]
+
+ def write_byte(self, address: int, value: int) -> None:
+ self.buffer[address] = value
+
+ def write_bytes(self, startaddress: int, values) -> None:
+ self.buffer[startaddress:startaddress + len(values)] = values
+
+ def write_int16(self, address: int, value: int) -> None:
+ self.write_bytes(address, (value & 0xFF, (value >> 8) & 0xFF))
+
+
+class TestEnemizerPatches(unittest.TestCase):
+ def test_enemizer_base_patch_applies_mimic_hooks(self) -> None:
+ rom = FakeRom()
+
+ apply_enemizer_base_patch(rom)
+
+ self.assertEqual(tuple(rom.read_bytes(0x307CB, 2)), (0xB6, 0x91))
+ self.assertEqual(tuple(rom.read_bytes(0x311B6, 4)), (0x22, 0x1A, 0x9A, 0x36))
+ self.assertEqual(tuple(rom.read_bytes(0x36C08, 5)), (0x22, 0x4E, 0x9A, 0x36, 0xEA))
+ self.assertEqual(tuple(rom.read_bytes(0x36DA6, 4)), (0x22, 0x66, 0x9A, 0x36))
+ self.assertEqual(tuple(rom.read_bytes(0xF0BB1, 2)), (0x95, 0xC7))
+ self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, 4)), (0xEA, 0xEA, 0xEA, 0xEA))
+ self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x00, 0x00))
+ self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x00)
+
+ def test_randomized_tile_trap_floor_tile_patch_is_separate(self) -> None:
+ rom = FakeRom()
+
+ _apply_randomized_tile_trap_floor_tile(rom)
+
+ self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x88, 0x01))
+ self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x12)
+
+ def test_enemy_shuffle_enables_hidden_enemy_and_mimic_support(self) -> None:
+ rom = FakeRom()
+ world = self._build_world(enemy_shuffle=True, bush_shuffle=False)
+
+ self._apply_native_enemizer_features(world, rom)
+
+ self.assertEqual(
+ tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(VANILLA_HIDDEN_ENEMY_CHANCE_POOL))),
+ VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
+ )
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x01)
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x01)
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x01)
+ self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x54, 0x9C))
+ self.assertEqual(rom.read_byte(0x1F2E5), 0xB0)
+ self.assertEqual(rom.read_byte(0x1F2EB), 0xD0)
+
+ def test_bush_shuffle_and_remaining_tables_are_patched_natively(self) -> None:
+ rom = FakeRom()
+ item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
+ not_item_sprite_address = _get_enemizer_symbol("notItemSprite_Mimic")
+ rom.write_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
+ rom.write_byte(item_table_address + 5, ARROW_REFILL_5_SPRITE_ID)
+ rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
+
+ included_hp_sprite_id = 0x01
+ included_damage_sprite_id = 0x02
+ excluded_sprite_id = min(EXCLUDED_ENEMY_TABLE_SPRITE_IDS)
+ rom.write_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id, 0x06)
+ rom.write_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id, 0x07)
+ rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id, 0x06)
+ rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id, 0x05)
+
+ world = self._build_world(
+ bush_shuffle=True,
+ killable_thieves=True,
+ enemy_health="hard",
+ enemy_damage="chaos",
+ )
+
+ self._apply_native_enemizer_features(world, rom)
+
+ self.assertEqual(
+ tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL))),
+ RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
+ )
+ self.assertEqual(rom.read_byte(item_table_address + 5), RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
+ self.assertEqual(rom.read_byte(not_item_sprite_address + 4), THIEF_SPRITE_ID)
+ self.assertNotEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 0x08)
+ self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 2)
+ self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 25)
+ self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 2)
+ self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 25)
+ self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id), 0x07)
+ self.assertIn(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id), range(8))
+ self.assertEqual(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id), 0x05)
+ for group_id in range(10):
+ group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
+ green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
+ self.assertIn(green_mail, range(64))
+ self.assertIn(blue_mail, range(64))
+ self.assertIn(red_mail, range(64))
+
+ def test_killable_thief_sets_default_hp_without_enemy_health_shuffle(self) -> None:
+ rom = FakeRom()
+ rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
+
+ world = self._build_world(killable_thieves=True)
+
+ self._apply_native_enemizer_features(world, rom)
+
+ self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), THIEF_DEFAULT_HP)
+
+ def test_bush_shuffle_without_enemy_shuffle_does_not_enable_sprite_randomization_flags(self) -> None:
+ rom = FakeRom()
+
+ self._apply_native_enemizer_features(self._build_world(bush_shuffle=True), rom)
+
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x00)
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x00)
+ self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x00)
+ self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x00, 0x00))
+ self.assertEqual(rom.read_byte(0x1F2E5), 0x00)
+ self.assertEqual(rom.read_byte(0x1F2EB), 0x00)
+
+ def test_non_chaos_enemy_damage_uses_expected_mail_scaling(self) -> None:
+ rom = FakeRom()
+
+ self._apply_native_enemizer_features(self._build_world(enemy_damage="hard"), rom)
+
+ for group_id in range(10):
+ group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
+ green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
+ self.assertEqual(blue_mail, green_mail * 3 // 4)
+ self.assertEqual(red_mail, green_mail * 3 // 8)
+
+ def test_patch_bosses_overwrites_enemy_shuffle_boss_room_graphics(self) -> None:
+ rom = FakeRom()
+ dungeon_header_base = _get_enemizer_symbol("room_header_table")
+ eastern_dungeon_data = DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]
+ rom.write_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3, BOSS_PATCH_DATA["Armos"].graphics)
+
+ for table_index in BOSS_GFX_SHEET_INDEXES.values():
+ rom.write_byte(0x4FC0 + table_index, 0xAA)
+ rom.write_byte(0x509F + table_index, 0xBB)
+ rom.write_byte(0x517E + table_index, 0xCC)
+
+ patch_bosses(self._build_boss_world({"Eastern Palace": "Vitreous"}), rom)
+
+ eastern_boss_data = BOSS_PATCH_DATA["Vitreous"]
+ self.assertEqual(
+ tuple(rom.read_bytes(eastern_dungeon_data.sprite_pointer_address, 2)),
+ eastern_boss_data.pointer,
+ )
+ self.assertEqual(
+ rom.read_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3),
+ eastern_boss_data.graphics,
+ )
+
+ for table_index in BOSS_GFX_SHEET_INDEXES.values():
+ self.assertEqual(rom.read_byte(0x4FC0 + table_index), 0xAA)
+ self.assertEqual(rom.read_byte(0x509F + table_index), 0xBB)
+ self.assertEqual(rom.read_byte(0x517E + table_index), 0xCC)
+
+ def test_native_enemizer_rng_is_deterministic_for_same_world_settings(self) -> None:
+ world = self._build_world(enemy_health="hard", enemy_damage="chaos", bush_shuffle=True)
+
+ rng_a = _make_native_enemizer_rng(world)
+ rng_b = _make_native_enemizer_rng(world)
+
+ self.assertEqual([rng_a.randrange(256) for _ in range(8)], [rng_b.randrange(256) for _ in range(8)])
+
+ @staticmethod
+ def _apply_native_enemizer_features(world: SimpleNamespace, rom: FakeRom) -> None:
+ enemy_shuffle_enabled = bool(world.options.enemy_shuffle)
+ bush_shuffle_enabled = bool(world.options.bush_shuffle)
+ enemy_health_key = _option_key(world.options.enemy_health)
+ enemy_damage_key = _option_key(world.options.enemy_damage)
+
+ if enemy_shuffle_enabled or bush_shuffle_enabled:
+ _set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
+ hidden_enemy_chance_pool = (
+ RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL if bush_shuffle_enabled else VANILLA_HIDDEN_ENEMY_CHANCE_POOL
+ )
+ rom.write_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
+ _update_hidden_enemy_item_table_for_retro_mode(rom)
+
+ if enemy_shuffle_enabled:
+ _set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
+ _set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
+ _set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
+ rom.write_bytes(0x1F2D5, (0x54, 0x9C))
+ rom.write_byte(0x1F2E5, 0xB0)
+ rom.write_byte(0x1F2EB, 0xD0)
+
+ if world.options.killable_thieves:
+ _apply_killable_thief(rom)
+
+ if enemy_health_key != "default" or enemy_damage_key != "default":
+ rng = _make_native_enemizer_rng(world)
+ else:
+ rng = None
+
+ if enemy_health_key != "default":
+ assert rng is not None
+ _randomize_enemy_health(rom, rng, enemy_health_key)
+
+ if enemy_damage_key != "default":
+ assert rng is not None
+ _randomize_enemy_damage(rom, rng, allow_zero_damage=True)
+ _shuffle_damage_groups(rom, rng, chaos_mode=enemy_damage_key == "chaos", allow_zero_damage=True)
+
+ @staticmethod
+ def _build_world(
+ *,
+ enemy_shuffle: bool = False,
+ bush_shuffle: bool = False,
+ killable_thieves: bool = False,
+ enemy_health: str = "default",
+ enemy_damage: str = "default",
+ ) -> SimpleNamespace:
+ return SimpleNamespace(
+ player=1,
+ multiworld=SimpleNamespace(seed=12345, seed_name="native-enemizer-test"),
+ options=SimpleNamespace(
+ enemy_shuffle=enemy_shuffle,
+ bush_shuffle=bush_shuffle,
+ killable_thieves=killable_thieves,
+ enemy_health=SimpleNamespace(current_key=enemy_health),
+ enemy_damage=SimpleNamespace(current_key=enemy_damage),
+ ),
+ )
+
+ @staticmethod
+ def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
+ boss_overrides = boss_overrides or {}
+
+ def boss(name: str) -> SimpleNamespace:
+ return SimpleNamespace(enemizer_name=name)
+
+ return SimpleNamespace(
+ options=SimpleNamespace(mode="open"),
+ dungeons={
+ "Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
+ "Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
+ "Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
+ "Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
+ "Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
+ "Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
+ "Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
+ "Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
+ "Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
+ "Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
+ "Ganons Tower": SimpleNamespace(
+ bosses={
+ "bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
+ "middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
+ "top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
+ }
+ ),
+ },
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/worlds/alttp/test/TestEnemyShuffle.py b/worlds/alttp/test/TestEnemyShuffle.py
new file mode 100644
index 000000000000..c16d85b4ebcb
--- /dev/null
+++ b/worlds/alttp/test/TestEnemyShuffle.py
@@ -0,0 +1,834 @@
+import unittest
+from types import SimpleNamespace
+import random
+
+from worlds.alttp.EnemyShuffle import (
+ DungeonEnemyRoom,
+ DungeonEnemySprite,
+ DungeonSpriteGroup,
+ EnemyShuffleState,
+ EnemySpriteRequirement,
+ OverworldEnemyArea,
+ OverworldEnemySprite,
+ RandomizedDungeonEnemyRoom,
+ RandomizedDungeonEnemySprite,
+ RandomizedOverworldEnemyArea,
+ RandomizedOverworldEnemySprite,
+ WALLMASTER_SPRITE_ID,
+ _load_dungeon_sprite_metadata,
+ _read_room_sprites,
+ get_possible_dungeon_sprite_groups,
+ _get_requirements_for_usable_dungeon_enemies,
+ _get_requirements_for_usable_overworld_enemies,
+ _get_randomizable_sprites_in_room,
+ _apply_selected_boss_group_requirements,
+ _randomize_overworld_groups,
+ _randomize_room_sprites,
+ _setup_required_overworld_groups,
+ can_spawn_in_room,
+ validate_enemy_shuffle_state,
+)
+
+
+class TestEnemyShuffleValidation(unittest.TestCase):
+ def test_curated_room_sprite_addresses_exclude_hera_basement_key_slot(self) -> None:
+ room_id = 135
+ sprite_table_address = 0x4E397
+ rom_bytes = bytearray(0x4E3C0)
+ rom_bytes[sprite_table_address] = 0
+ room_135_sprite_records = (
+ (0x4E398, 0x05, 0x14, 0x18),
+ (0x4E39B, 0x07, 0x1A, 0x18),
+ (0x4E39E, 0x0B, 0x13, 0x18),
+ (0x4E3A1, 0x19, 0x06, 0x18),
+ (0x4E3A4, 0x08, 0xE7, 0x14),
+ (0x4E3A7, 0x04, 0x17, 0x1E),
+ (0x4E3AA, 0x0C, 0x03, 0x1E),
+ (0x4E3AD, 0x15, 0x04, 0x1E),
+ (0x4E3B0, 0x17, 0x0B, 0xA7),
+ (0x4E3B3, 0x18, 0x19, 0xA7),
+ (0x4E3B6, 0x19, 0x04, 0xA7),
+ (0x4E3B9, 0x1A, 0x08, 0xE4),
+ (0x4E3BC, 0x1C, 0x15, 0xA7),
+ )
+ for address, byte_0, byte_1, sprite_id in room_135_sprite_records:
+ rom_bytes[address] = byte_0
+ rom_bytes[address + 1] = byte_1
+ rom_bytes[address + 2] = sprite_id
+ rom_bytes[0x4E3BF] = 0xFF
+
+ sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, _load_dungeon_sprite_metadata())
+ sprite_addresses = {sprite.address for sprite in sprites}
+
+ self.assertNotIn(0x4E3B9, sprite_addresses)
+ self.assertIn(0x4E3B6, sprite_addresses)
+ self.assertFalse(any(sprite.has_key for sprite in sprites))
+
+ def test_curated_room_sprite_addresses_deduplicate_duplicate_slots(self) -> None:
+ room_id = 125
+ sprite_table_address = 0x4E2CA
+ metadata = _load_dungeon_sprite_metadata()
+ max_sprite_id_address = max(metadata["room_sprite_id_addresses"][room_id])
+ rom_bytes = bytearray(max_sprite_id_address + 2)
+ rom_bytes[sprite_table_address] = 0
+ for offset, sprite_id_address in enumerate(metadata["room_sprite_id_addresses"][room_id]):
+ address = sprite_id_address - 2
+ sprite_id = 0x80 if offset % 2 == 0 else 0x81
+ rom_bytes[address] = 0
+ rom_bytes[address + 1] = 0
+ rom_bytes[address + 2] = sprite_id
+
+ sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, metadata)
+ sprite_addresses = [sprite.address for sprite in sprites]
+
+ self.assertEqual(len(sprite_addresses), len(set(sprite_addresses)))
+
+ def test_rejects_non_killable_shutter_room(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=1,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x10, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=True,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={1: room},
+ randomized_dungeon_rooms={
+ 1: RandomizedDungeonEnemyRoom(
+ room_id=1,
+ room_header_address=0,
+ sprite_table_address=0,
+ original_graphics_block_id=1,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ RandomizedDungeonEnemySprite(
+ address=0x1000,
+ byte_0=0,
+ byte_1=0,
+ original_sprite_id=0x10,
+ sprite_id=0x11,
+ is_overlord=False,
+ has_key=False,
+ ),
+ ),
+ skipped_randomization=False,
+ )
+ },
+ sprite_requirements=(
+ self._requirement(0x10, killable=True, subgroup_0=(1,)),
+ self._requirement(0x11, killable=False, subgroup_0=(1,)),
+ ),
+ )
+
+ with self.assertRaises(ValueError):
+ validate_enemy_shuffle_state(state, is_standard_mode=False)
+
+ def test_rejects_water_enemy_in_non_water_room(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=165,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=True,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={165: room},
+ randomized_dungeon_rooms={
+ 165: RandomizedDungeonEnemyRoom(
+ room_id=165,
+ room_header_address=0,
+ sprite_table_address=0,
+ original_graphics_block_id=1,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ RandomizedDungeonEnemySprite(
+ address=0x1000,
+ byte_0=0,
+ byte_1=0,
+ original_sprite_id=0x20,
+ sprite_id=0x81,
+ is_overlord=False,
+ has_key=False,
+ ),
+ ),
+ skipped_randomization=False,
+ )
+ },
+ sprite_requirements=(
+ self._requirement(0x20, killable=True, subgroup_0=(1,)),
+ self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
+ ),
+ )
+
+ with self.assertRaisesRegex(ValueError, "water enemy"):
+ validate_enemy_shuffle_state(state, is_standard_mode=False)
+
+ def test_rejects_multiple_flopping_fish(self) -> None:
+ area = OverworldEnemyArea(
+ area_id=0x10,
+ sprite_table_address=0,
+ graphics_block_address=0,
+ graphics_block_id=1,
+ bush_sprite_id=0x20,
+ sprites=(
+ OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
+ OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
+ ),
+ do_not_randomize=False,
+ )
+ state = self._build_state(
+ overworld_areas={0x10: area},
+ randomized_overworld_areas={
+ 0x10: RandomizedOverworldEnemyArea(
+ area_id=0x10,
+ sprite_table_address=0,
+ graphics_block_address=0,
+ original_graphics_block_id=1,
+ graphics_block_id=1,
+ original_bush_sprite_id=0x20,
+ bush_sprite_id=0xD2,
+ sprites=(
+ RandomizedOverworldEnemySprite(
+ address=0x2000,
+ y_coord=0,
+ x_coord=0,
+ original_sprite_id=0x20,
+ sprite_id=0xD2,
+ ),
+ RandomizedOverworldEnemySprite(
+ address=0x2003,
+ y_coord=0,
+ x_coord=0,
+ original_sprite_id=0x21,
+ sprite_id=0xD2,
+ ),
+ ),
+ skipped_randomization=False,
+ )
+ },
+ sprite_requirements=(
+ self._requirement(0x20, group_ids=(1,)),
+ self._requirement(0x21, group_ids=(1,)),
+ self._requirement(0x22, group_ids=(1,)),
+ self._requirement(0xD2, group_ids=(1,)),
+ ),
+ )
+
+ with self.assertRaises(ValueError):
+ validate_enemy_shuffle_state(state, is_standard_mode=False)
+
+ def test_allows_multiple_flopping_fish_when_no_other_sprite_is_possible(self) -> None:
+ area = OverworldEnemyArea(
+ area_id=0x10,
+ sprite_table_address=0,
+ graphics_block_address=0,
+ graphics_block_id=1,
+ bush_sprite_id=0x20,
+ sprites=(
+ OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
+ OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
+ ),
+ do_not_randomize=False,
+ )
+ state = self._build_state(
+ overworld_areas={0x10: area},
+ randomized_overworld_areas={
+ 0x10: RandomizedOverworldEnemyArea(
+ area_id=0x10,
+ sprite_table_address=0,
+ graphics_block_address=0,
+ original_graphics_block_id=1,
+ graphics_block_id=1,
+ original_bush_sprite_id=0x20,
+ bush_sprite_id=0xD2,
+ sprites=(
+ RandomizedOverworldEnemySprite(
+ address=0x2000,
+ y_coord=0,
+ x_coord=0,
+ original_sprite_id=0x20,
+ sprite_id=0xD2,
+ ),
+ RandomizedOverworldEnemySprite(
+ address=0x2003,
+ y_coord=0,
+ x_coord=0,
+ original_sprite_id=0x21,
+ sprite_id=0xD2,
+ ),
+ ),
+ skipped_randomization=False,
+ )
+ },
+ sprite_requirements=(
+ self._requirement(0x20, group_ids=(2,)),
+ self._requirement(0x21, group_ids=(2,)),
+ self._requirement(0xD2, group_ids=(1,)),
+ ),
+ )
+
+ validate_enemy_shuffle_state(state, is_standard_mode=False)
+
+ def test_excludes_absorbables_from_usable_enemy_pools(self) -> None:
+ state = self._build_state(
+ sprite_requirements=(
+ self._requirement(0x10, subgroup_0=(1,)),
+ self._requirement(0xE3, subgroup_0=(1,), absorbable=True),
+ self._requirement(0x20, subgroup_0=(1,), never_use_dungeon=True),
+ self._requirement(0x21, subgroup_0=(1,), never_use_overworld=True),
+ ),
+ )
+
+ self.assertEqual(
+ [requirement.sprite_id for requirement in _get_requirements_for_usable_dungeon_enemies(state)],
+ [0x10, 0x21],
+ )
+ self.assertEqual(
+ [requirement.sprite_id for requirement in _get_requirements_for_usable_overworld_enemies(state)],
+ [0x10, 0x20],
+ )
+
+ def test_key_enemy_replacements_exclude_moblins(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=1,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x12, is_overlord=False, has_key=True),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=False,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={1: room},
+ sprite_requirements=(
+ self._requirement(0x12, killable=True, subgroup_0=(1,), cannot_have_key=True),
+ self._requirement(0x13, killable=True, subgroup_0=(1,)),
+ ),
+ )
+ selected_group = state.sprite_groups[0x41]
+
+ randomized_room = _randomize_room_sprites(
+ SimpleNamespace(random=random.Random(0)),
+ state,
+ room,
+ selected_group,
+ False,
+ )
+
+ self.assertEqual(randomized_room.sprites[0].sprite_id, 0x13)
+
+ def test_shutter_water_room_prefers_killable_water_enemy(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=40,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x8A, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=True,
+ is_water_room=True,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={40: room},
+ sprite_requirements=(
+ self._requirement(0x8A, killable=False, subgroup_2=(34,)),
+ self._requirement(0x81, killable=True, subgroup_2=(34,), is_water_sprite=True),
+ self._requirement(0x9A, killable=False, subgroup_2=(34,), is_water_sprite=True),
+ ),
+ )
+ selected_group = state.sprite_groups[0x41]
+ selected_group.subgroup_2 = 34
+
+ randomized_room = _randomize_room_sprites(
+ SimpleNamespace(random=random.Random(0)),
+ state,
+ room,
+ selected_group,
+ False,
+ )
+
+ self.assertEqual(randomized_room.sprites[0].sprite_id, 0x81)
+
+ def test_non_water_shutter_room_replacements_exclude_water_enemies(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=165,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=True,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={165: room},
+ sprite_requirements=(
+ self._requirement(0x20, killable=False, subgroup_0=(1,)),
+ self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
+ self._requirement(0x22, killable=True, subgroup_0=(1,)),
+ ),
+ )
+
+ randomized_room = _randomize_room_sprites(
+ SimpleNamespace(random=random.Random(1)),
+ state,
+ room,
+ state.sprite_groups[0x41],
+ False,
+ )
+
+ self.assertEqual(randomized_room.sprites[0].sprite_id, 0x22)
+
+ def test_non_water_shutter_group_selection_requires_non_water_killable_enemy(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=165,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=True,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={165: room},
+ sprite_requirements=(
+ self._requirement(0x20, killable=False, subgroup_0=(1,)),
+ self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
+ ),
+ )
+
+ self.assertEqual(get_possible_dungeon_sprite_groups(state, room), tuple())
+
+ def test_wallmaster_cannot_spawn_in_high_room_ids(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=0x100,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=tuple(),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=False,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+
+ self.assertFalse(can_spawn_in_room(self._requirement(WALLMASTER_SPRITE_ID), room))
+
+ def test_room_specific_do_not_randomize_sprites_are_not_updated(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=7,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x30, is_overlord=False, has_key=False),
+ DungeonEnemySprite(address=0x1003, byte_0=0, byte_1=0, sprite_id=0x31, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=False,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={7: room},
+ sprite_requirements=(
+ self._requirement(0x30, subgroup_0=(1,), dont_randomize_rooms=(7,)),
+ self._requirement(0x31, subgroup_0=(1,)),
+ ),
+ )
+
+ self.assertEqual(
+ [sprite.sprite_id for sprite in _get_randomizable_sprites_in_room(state, room)],
+ [0x31],
+ )
+
+ def test_water_rooms_only_use_water_enemies(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=1,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=False,
+ is_water_room=True,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={1: room},
+ sprite_requirements=(
+ self._requirement(0x20, subgroup_0=(1,)),
+ self._requirement(0x21, subgroup_0=(1,), is_water_sprite=True),
+ self._requirement(0x22, subgroup_0=(1,), is_water_sprite=True),
+ ),
+ )
+
+ randomized_room = _randomize_room_sprites(
+ SimpleNamespace(random=random.Random(0)),
+ state,
+ room,
+ state.sprite_groups[0x41],
+ False,
+ )
+
+ self.assertIn(randomized_room.sprites[0].sprite_id, {0x21, 0x22})
+
+ def test_dungeon_group_selection_excludes_groups_without_enemy_requirements(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=1,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=False,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={1: room},
+ sprite_requirements=(self._requirement(0x20, subgroup_0=(1,)),),
+ )
+ state.sprite_groups[0x42] = DungeonSpriteGroup(
+ group_id=0x42,
+ dungeon_group_id=2,
+ subgroup_0=0,
+ subgroup_1=0,
+ subgroup_2=0,
+ subgroup_3=0,
+ )
+
+ possible_groups = get_possible_dungeon_sprite_groups(state, room)
+
+ self.assertEqual([group.group_id for group in possible_groups], [0x41])
+
+ def test_key_room_group_selection_excludes_groups_without_room_spawnable_key_enemies(self) -> None:
+ room = DungeonEnemyRoom(
+ room_id=61,
+ room_header_address=0,
+ sprite_table_address=0,
+ graphics_block_id=1,
+ tag_1=0,
+ tag_2=0,
+ sort_sprites_value=0,
+ sprites=(
+ DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=True),
+ ),
+ required_group_id=None,
+ required_subgroup_0=tuple(),
+ required_subgroup_1=tuple(),
+ required_subgroup_2=tuple(),
+ required_subgroup_3=tuple(),
+ is_shutter_room=False,
+ is_water_room=False,
+ do_not_randomize=False,
+ no_special_enemies_standard=False,
+ )
+ state = self._build_state(
+ dungeon_rooms={61: room},
+ sprite_requirements=(
+ self._requirement(0x20, subgroup_0=(1,)),
+ self._requirement(0x50, killable=True, subgroup_1=(32,), excluded_rooms=(61,)),
+ self._requirement(0x9C, killable=True, subgroup_1=(32,), cannot_have_key=True),
+ self._requirement(0x51, killable=True, subgroup_1=(33,)),
+ ),
+ )
+ state.sprite_groups[0x41] = DungeonSpriteGroup(
+ group_id=0x41,
+ dungeon_group_id=1,
+ subgroup_0=1,
+ subgroup_1=32,
+ subgroup_2=1,
+ subgroup_3=1,
+ )
+ state.sprite_groups[0x42] = DungeonSpriteGroup(
+ group_id=0x42,
+ dungeon_group_id=2,
+ subgroup_0=1,
+ subgroup_1=33,
+ subgroup_2=1,
+ subgroup_3=1,
+ )
+
+ possible_groups = get_possible_dungeon_sprite_groups(state, room)
+
+ self.assertEqual([group.group_id for group in possible_groups], [0x42])
+
+ def test_overworld_group_randomization_preserves_forced_subgroups(self) -> None:
+ sprite_groups = {
+ 7: DungeonSpriteGroup(group_id=7, dungeon_group_id=-57, subgroup_0=1, subgroup_1=2, subgroup_2=3, subgroup_3=4),
+ }
+
+ _setup_required_overworld_groups(
+ sprite_groups,
+ (
+ SimpleNamespace(
+ group_id=7,
+ subgroup_0=None,
+ subgroup_1=None,
+ subgroup_2=None,
+ subgroup_3=17,
+ areas=(0x02,),
+ ),
+ ),
+ )
+ _randomize_overworld_groups(SimpleNamespace(random=random.Random(0)), sprite_groups)
+
+ group = sprite_groups[7]
+ self.assertEqual(group.subgroup_3, 17)
+ self.assertIn(group.subgroup_0, {22, 31, 47, 14})
+ self.assertIn(group.subgroup_1, {44, 30, 32})
+ self.assertIn(group.subgroup_2, {12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42})
+
+ def test_selected_boss_group_requirements_override_shared_boss_graphics_group(self) -> None:
+ sprite_groups = {
+ 0x56: DungeonSpriteGroup(
+ group_id=0x56,
+ dungeon_group_id=22,
+ subgroup_0=1,
+ subgroup_1=1,
+ subgroup_2=60,
+ subgroup_3=49,
+ ),
+ }
+ sprite_requirements = (
+ self._requirement(162, subgroup_2=(60,)),
+ self._requirement(189, subgroup_3=(61,)),
+ )
+
+ _apply_selected_boss_group_requirements(
+ self._build_boss_world({"Eastern Palace": "Vitreous"}),
+ sprite_groups,
+ sprite_requirements,
+ )
+
+ group = sprite_groups[0x56]
+ self.assertEqual(group.subgroup_2, 60)
+ self.assertEqual(group.subgroup_3, 61)
+ self.assertTrue(group.preserve_subgroup_2)
+ self.assertTrue(group.preserve_subgroup_3)
+
+ @staticmethod
+ def _requirement(
+ sprite_id: int,
+ *,
+ killable: bool = False,
+ subgroup_0: tuple[int, ...] = tuple(),
+ subgroup_1: tuple[int, ...] = tuple(),
+ subgroup_2: tuple[int, ...] = tuple(),
+ subgroup_3: tuple[int, ...] = tuple(),
+ group_ids: tuple[int, ...] = tuple(),
+ absorbable: bool = False,
+ never_use_dungeon: bool = False,
+ never_use_overworld: bool = False,
+ cannot_have_key: bool = False,
+ is_water_sprite: bool = False,
+ excluded_rooms: tuple[int, ...] = tuple(),
+ dont_randomize_rooms: tuple[int, ...] = tuple(),
+ ) -> EnemySpriteRequirement:
+ return EnemySpriteRequirement(
+ sprite_name=f"sprite_{sprite_id:02x}",
+ sprite_id=sprite_id,
+ boss=False,
+ overlord=False,
+ do_not_randomize=False,
+ killable=killable,
+ npc=False,
+ never_use_dungeon=never_use_dungeon,
+ never_use_overworld=never_use_overworld,
+ cannot_have_key=cannot_have_key,
+ is_object=False,
+ absorbable=absorbable,
+ is_water_sprite=is_water_sprite,
+ is_enemy_sprite=True,
+ group_ids=group_ids,
+ subgroup_0=subgroup_0,
+ subgroup_1=subgroup_1,
+ subgroup_2=subgroup_2,
+ subgroup_3=subgroup_3,
+ parameters=None,
+ special_glitched=False,
+ excluded_rooms=excluded_rooms,
+ dont_randomize_rooms=dont_randomize_rooms,
+ spawnable_rooms=tuple(),
+ )
+
+ @staticmethod
+ def _build_state(
+ *,
+ dungeon_rooms=None,
+ overworld_areas=None,
+ randomized_dungeon_rooms=None,
+ randomized_overworld_areas=None,
+ sprite_requirements=tuple(),
+ ) -> EnemyShuffleState:
+ sprite_groups = {
+ 1: DungeonSpriteGroup(group_id=1, dungeon_group_id=-63, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
+ 0x41: DungeonSpriteGroup(group_id=0x41, dungeon_group_id=1, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
+ }
+ return EnemyShuffleState(
+ dungeon_rooms=dungeon_rooms or {},
+ overworld_areas=overworld_areas or {},
+ sprite_groups=sprite_groups,
+ sprite_requirements=sprite_requirements,
+ room_group_requirements=tuple(),
+ overworld_group_requirements=tuple(),
+ shutter_room_ids=frozenset(),
+ water_room_ids=frozenset(),
+ dont_randomize_room_ids=frozenset(),
+ no_special_enemies_standard_room_ids=frozenset(),
+ boss_room_ids=frozenset(),
+ dont_randomize_overworld_area_ids=frozenset(),
+ randomized_dungeon_rooms=randomized_dungeon_rooms or {},
+ randomized_overworld_areas=randomized_overworld_areas or {},
+ )
+
+ @staticmethod
+ def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
+ boss_overrides = boss_overrides or {}
+
+ def boss(name: str) -> SimpleNamespace:
+ return SimpleNamespace(enemizer_name=name)
+
+ return SimpleNamespace(
+ options=SimpleNamespace(mode="open"),
+ dungeons={
+ "Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
+ "Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
+ "Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
+ "Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
+ "Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
+ "Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
+ "Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
+ "Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
+ "Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
+ "Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
+ "Ganons Tower": SimpleNamespace(
+ bosses={
+ "bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
+ "middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
+ "top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
+ }
+ ),
+ },
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/worlds/alttp/test/TestPotShuffle.py b/worlds/alttp/test/TestPotShuffle.py
new file mode 100644
index 000000000000..5d63d5d182c7
--- /dev/null
+++ b/worlds/alttp/test/TestPotShuffle.py
@@ -0,0 +1,56 @@
+import random
+import unittest
+from types import SimpleNamespace
+
+from worlds.alttp.PotShuffle import (
+ POT_KEY,
+ POT_HOLE,
+ generate_pot_shuffle,
+ get_unique_pot_item_position,
+)
+
+
+class TestPotShuffle(unittest.TestCase):
+ def test_reserved_key_rooms_only_place_actual_keys(self) -> None:
+ for seed in range(10):
+ world = SimpleNamespace(
+ random=random.Random(seed),
+ options=SimpleNamespace(retro_bow=False),
+ )
+ shuffled_pots = generate_pot_shuffle(world)
+ conveyor_cross_keys = [
+ pot for pot in shuffled_pots[0x8B]
+ if pot.item == POT_KEY
+ ]
+ self.assertEqual(len(conveyor_cross_keys), 1)
+
+ def test_get_unique_pot_item_position_returns_single_match(self) -> None:
+ world = SimpleNamespace(
+ random=random.Random(0),
+ options=SimpleNamespace(retro_bow=False),
+ )
+ shuffled_pots = generate_pot_shuffle(world)
+
+ self.assertEqual(
+ get_unique_pot_item_position(shuffled_pots, 0x36, POT_KEY),
+ (114, 16),
+ )
+
+ def test_reserved_hole_room_keeps_hole_fixed(self) -> None:
+ for seed in range(25):
+ world = SimpleNamespace(
+ random=random.Random(seed),
+ options=SimpleNamespace(retro_bow=False),
+ )
+ shuffled_pots = generate_pot_shuffle(world)
+ hole_positions = [
+ (pot.x, pot.y)
+ for pot in shuffled_pots[206]
+ if pot.item == POT_HOLE
+ ]
+
+ self.assertEqual(hole_positions, [(204, 11)])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/worlds/apquest/archipelago.json b/worlds/apquest/archipelago.json
index 6a6c3ddd02c3..6392e0693041 100644
--- a/worlds/apquest/archipelago.json
+++ b/worlds/apquest/archipelago.json
@@ -1,6 +1,6 @@
{
"game": "APQuest",
- "minimum_ap_version": "0.6.4",
- "world_version": "1.0.1",
+ "minimum_ap_version": "0.6.7",
+ "world_version": "2.0.0",
"authors": ["NewSoupVi"]
}
diff --git a/worlds/apquest/client/ap_quest_client.py b/worlds/apquest/client/ap_quest_client.py
index bd67cc55ca3b..e875da7b68ca 100644
--- a/worlds/apquest/client/ap_quest_client.py
+++ b/worlds/apquest/client/ap_quest_client.py
@@ -198,7 +198,7 @@ def render(self) -> None:
if self.ap_quest_game is None:
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
- self.ui.render(self.ap_quest_game, self.player_sprite)
+ self.ui.render(self.ap_quest_game, self.player_sprite, self.hard_mode)
self.handle_game_events()
def location_checked_side_effects(self, location: int) -> None:
diff --git a/worlds/apquest/client/game_manager.py b/worlds/apquest/client/game_manager.py
index ed2793da36a9..7d40a36ee3a7 100644
--- a/worlds/apquest/client/game_manager.py
+++ b/worlds/apquest/client/game_manager.py
@@ -88,23 +88,23 @@ def game_started(self) -> None:
self.game_view.force_focus()
self.sound_manager.game_started = True
- def render(self, game: Game, player_sprite: PlayerSprite) -> None:
+ def render(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
self.setup_game_grid_if_not_setup(game)
# This calls game.render(), which needs to happen to update the state of math traps
- self.render_gameboard(game, player_sprite)
+ self.render_gameboard(game, player_sprite, hard_mode)
# Only now can we check whether a math problem is active
self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None)
self.sound_manager.math_trap_active = game.active_math_problem is not None
self.render_item_column(game)
- def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None:
+ def render_gameboard(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
rendered_gameboard = game.render()
for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False):
for graphic, image in zip(gameboard_row, image_row[:11], strict=False):
- texture = get_texture(graphic, player_sprite)
+ texture = get_texture(graphic, player_sprite, hard_mode)
if texture is None:
image.opacity = 0
diff --git a/worlds/apquest/client/graphics.py b/worlds/apquest/client/graphics.py
index 0e31218c6f02..9acf7c99878e 100644
--- a/worlds/apquest/client/graphics.py
+++ b/worlds/apquest/client/graphics.py
@@ -29,6 +29,7 @@ class RelatedTexture(NamedTuple):
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
+ # Inanimates
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
@@ -37,29 +38,25 @@ class RelatedTexture(NamedTuple):
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
-
+ # Enemies
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
-
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
-
+ # Items
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
-
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
-
- Graphic.ITEMS_TEXT: "items_text.png",
-
+ # Numbers
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
@@ -70,26 +67,29 @@ class RelatedTexture(NamedTuple):
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
-
+ # Letters
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
-
+ # Mathematical symbols
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
+ # Other visual-only elements
+ Graphic.ITEMS_TEXT: "items_text.png",
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
-
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
}
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
+EASY_MODE_BOSS_2_HEALTH = RelatedTexture("boss.png", 16, 0, 16, 16)
+
class PlayerSprite(Enum):
HUMAN = 0
@@ -160,13 +160,18 @@ def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Textu
return sub_texture
-def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None:
+def get_texture(
+ graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None, hard_mode: bool = False
+) -> Texture | None:
if graphic == Graphic.EMPTY:
return None
if graphic == "Grass":
return get_texture_by_identifier(BACKGROUND_TILE)
+ if graphic == Graphic.BOSS_2_HEALTH and not hard_mode:
+ return get_texture_by_identifier(EASY_MODE_BOSS_2_HEALTH)
+
if graphic in IMAGE_GRAPHICS:
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
diff --git a/worlds/apquest/game/gameboard.py b/worlds/apquest/game/gameboard.py
index 77688c2929f9..e034b3f71c26 100644
--- a/worlds/apquest/game/gameboard.py
+++ b/worlds/apquest/game/gameboard.py
@@ -246,7 +246,7 @@ def create_gameboard(hard_mode: bool, hammer_exists: bool, extra_chest: bool) ->
breakable_block = BreakableBlock() if hammer_exists else Empty()
normal_enemy = EnemyWithLoot(2 if hard_mode else 1, Location.ENEMY_DROP)
- boss = FinalBoss(5 if hard_mode else 3)
+ boss = FinalBoss(5 if hard_mode else 2)
gameboard = (
(Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()),
diff --git a/worlds/apquest/game/graphics/boss.png b/worlds/apquest/game/graphics/boss.png
index dbcda31048e2..07a9af35c0f1 100644
Binary files a/worlds/apquest/game/graphics/boss.png and b/worlds/apquest/game/graphics/boss.png differ
diff --git a/worlds/apquest/rules.py b/worlds/apquest/rules.py
index 533c33d5eac2..02c76daefcb8 100644
--- a/worlds/apquest/rules.py
+++ b/worlds/apquest/rules.py
@@ -2,12 +2,16 @@
from typing import TYPE_CHECKING
-from BaseClasses import CollectionState
-from worlds.generic.Rules import add_rule, set_rule
+from rule_builder.options import OptionFilter
+from rule_builder.rules import Has, HasAll, Rule
+
+from .options import HardMode
if TYPE_CHECKING:
from .world import APQuestWorld
+HAS_KEY = Has("Key") # Hmm, what could this be? A little foreshadowing perhaps? :) You'll find out if you keep reading!
+
def set_all_rules(world: APQuestWorld) -> None:
# In order for AP to generate an item layout that is actually possible for the player to complete,
@@ -26,36 +30,46 @@ def set_all_entrance_rules(world: APQuestWorld) -> None:
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
- # An access rule is a function. We can define this function like any other function.
- # This function must accept exactly one parameter: A "CollectionState".
- # A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
- # which regions they've reached, etc.
- # In an access rule, we can ask whether the player has a collected a certain item.
- # We can do this via the state.has(...) function.
- # This function takes an item name, a player number, and an optional count parameter (more on that below)
- # Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
- # our function needs to be locally defined so that it has access to the player number from the outer scope.
- # In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
- def can_destroy_bush(state: CollectionState) -> bool:
- return state.has("Sword", world.player)
-
- # Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
- # One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
- set_rule(overworld_to_bottom_right_room, can_destroy_bush)
-
- # Because the function has to be defined locally, most worlds prefer the lambda syntax.
- set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
-
- # Conditions can depend on event items.
- set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
+ # Now, let's make some rules!
+ # First, let's handle the transition from the overworld to the bottom right room,
+ # which requires slashing a bush with the Sword.
+ # For this, we need a rule that says "player has a Sword".
+ # We can use a "Has"-type rule from the rule_builder module for this.
+ can_destroy_bush = Has("Sword")
+
+ # Now we can set our "can_destroy_bush" rule to the entrance which requires slashing a bush to clear the path.
+ # The easiest way to do this is by calling world.set_rule, which works for both Locations and Entrances.
+ world.set_rule(overworld_to_bottom_right_room, can_destroy_bush)
+
+ # Conditions can also depend on event items.
+ button_pressed = Has("Top Left Room Button Pressed")
+ world.set_rule(right_room_to_final_boss_room, button_pressed)
# Some entrance rules may only apply if the player enabled certain options.
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
# Overworld to the Top Middle Room.
if world.options.hammer:
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
- set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
-
+ can_smash_brick = Has("Hammer")
+ world.set_rule(overworld_to_top_middle_room, can_smash_brick)
+
+ # So far, we've been using "Has" from the Rule Builder to make our rules.
+ # There is another way to make rules that you will see in a lot of older worlds.
+ # A rule can just be a function that takes a "state" argument and returns a bool.
+ # As a demonstration of what that looks like, let's do it with our final Entrance rule:
+ world.set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
+ # This style is not really recommended anymore, though.
+ # Notice how you have to explicitly capture world.player here so that the rule applies to the correct player?
+ # Well, Rule Builder does this part for you, inside of world.set_rule.
+ # This doesn't just result in shorter code, it also means you can define rules statically (at the module level).
+ # APQuest opts to create its Rule objects locally, but just to show what this would look like,
+ # we'll re-set the "Overworld to Top Left Room" rule to a constant defined at the top of this file:
+ world.set_rule(overworld_to_top_left_room, HAS_KEY)
+
+ # Beyond these structural advantages,
+ # Rule Builder also allows the core AP code to do a lot of under-the-hood optimizations.
+ # Rule Builder is quite comprehensive, and even if you have really esoteric rules,
+ # you can make custom rules by subclassing CustomRule.
def set_all_location_rules(world: APQuestWorld) -> None:
# Location rules work no differently from Entrance rules.
@@ -67,65 +81,72 @@ def set_all_location_rules(world: APQuestWorld) -> None:
# So, we need to set requirements on the Locations themselves.
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
- # Sometimes, you may want to have different rules depending on the player's chosen options.
- # There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
- right_room_enemy = world.get_location("Right Room Enemy Drop")
-
- # DON'T DO THIS!!!!
- set_rule(
- right_room_enemy,
- lambda state: (
- state.has("Sword", world.player)
- and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
- ),
- )
- # DON'T DO THIS!!!!
-
- # Now, what's actually wrong with this? It works perfectly fine, right?
- # If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
- # The access rule we just wrote does this correctly, so what's the problem?
- # The problem is performance.
- # Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
- # However, access rules in particular are by far the hottest code path in Archipelago.
- # An access rule will potentially be called thousands or even millions of times over the course of one generation.
- # As a result, access rules are the one place where it's really worth putting in some effort to optimize.
- # What's the performance problem here?
- # Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
- # Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
- # Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
- # Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
- # and instead having two *different* set_rule calls depending on which case we're in.
+ # In "set_all_entrance_rules", we had a rule for a location that doesn't always exist.
+ # In this case, we had to check for its existence (by checking the player's chosen options) before setting the rule.
+ # Other times, you may have a situation where a location can have two different rules depending on the options.
+ # In our case, the enemy in the right room has more health if hard mode is selected,
+ # so ontop of the Sword, the player will either need one more health or a Shield in hard mode.
+ # First, let's make our sword condition.
+ can_defeat_basic_enemy: Rule = Has("Sword")
+ # Next, we'll check whether hard mode has been chosen in the player options.
if world.options.hard_mode:
- # If you have multiple conditions, you can obviously chain them via "or" or "and".
- # However, there are also the nice helper functions "state.has_any" and "state.has_all".
- set_rule(
- right_room_enemy,
- lambda state: (
- state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
- ),
- )
- else:
- set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
-
- # Another way to chain multiple conditions is via the add_rule function.
- # This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
- # In our case, it's pretty useful because hard mode and easy mode have different requirements.
- final_boss = world.get_location("Final Boss Defeated")
+ # We'll make the condition for "Has a Shield or a Health Upgrade".
+ # We can chain two "Has" conditions together with the | operator to make "Has Shield or has Health Upgrade".
+ can_withstand_a_hit = Has("Shield") | Has("Health Upgrade")
- # For the "known" requirements, it's still better to chain them using a normal "and" condition.
- add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
+ # Now, we chain this rule to our Sword rule.
+ # Since we want both conditions to be true, in this case, we have to chain them in an "and" way.
+ # For this, we can use the & operator.
+ can_defeat_basic_enemy = can_defeat_basic_enemy & can_withstand_a_hit
- if world.options.hard_mode:
- # You can check for multiple copies of an item by using the optional count parameter of state.has().
- add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
+ # Finally, we set our rule onto the Right Room Eney Drop location.
+ right_room_enemy = world.get_location("Right Room Enemy Drop")
+ world.set_rule(right_room_enemy, can_defeat_basic_enemy)
+
+ # For the final boss, we also need to chain multiple conditions.
+ # First of all, you always need a Sword and a Shield.
+ # So far, we used the | and & operators to chain "Has" rules.
+ # Instead, we can also use HasAny for an or-chain of items, or HasAll for an and-chain of items.
+ has_sword_and_shield: Rule = HasAll("Sword", "Shield")
+
+ # In hard mode, the player also needs both Health Upgrades to survive long enough to defeat the boss.
+ # For this, we can use the optional "count" parameter for "Has".
+ has_both_health_upgrades = Has("Health Upgrade", count=2)
+
+ # Previously, we used an "if world.options.hard_mode" condition to check if we should apply the extra requirement.
+ # However, if you're comfortable with boolean logic, there is another way.
+ # OptionFilter is a rule component which isn't a "Rule" on its own, but when used in a boolean expression with
+ # rules, it acts like True if the option has the specified value, and acts like False otherwise.
+ hard_mode_is_off = OptionFilter(HardMode, False)
+
+ # So with this option-checking rule component in hand, we can write our boss condition like this:
+ can_defeat_final_boss = has_sword_and_shield & (hard_mode_is_off | has_both_health_upgrades)
+ # If you're not as comfortable with boolean logic, it might be somewhat confusing why this is correct.
+ # There is nothing wrong with using "if" conditions to check for options, if you find that easier to understand.
+
+ # Finally, we apply the rule to our "Final Boss Defeated" event location.
+ final_boss = world.get_location("Final Boss Defeated")
+ world.set_rule(final_boss, can_defeat_final_boss)
def set_completion_condition(world: APQuestWorld) -> None:
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
+ # For this, we can use world.set_completion_rule.
# You can just set a completion condition directly like any other condition, referencing items the player receives:
- world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player)
+ world.set_completion_rule(HasAll("Sword", "Shield"))
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
# So lets undo what we just did, and instead set the completion condition to:
- world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
+ world.set_completion_rule(Has("Victory"))
+
+
+# One final comment about rules:
+# If your world exclusively uses Rule Builder rules (like APQuest), it's worth trying CachedRuleBuilderWorld.
+# CachedRuleBuilderWorld is a subclass of World that has a bunch of caching magic to make rules faster.
+# Just have your world class subclass CachedRuleBuilderWorld instead of World:
+# class APQuestWorld(CachedRuleBuilderWorld): ...
+# This may speed up your world, or it may make it slower.
+# The exact factors are complex and not well understood, but there is no harm in trying it.
+# Generate a few seeds and see if there is a noticeable difference!
+# If you're wondering, author has checked: APQuest is too simple to see any benefits, so we'll stick with "World".
diff --git a/worlds/blasphemous/Items.py b/worlds/blasphemous/Items.py
index 4843a99f2d12..70c006402ad3 100644
--- a/worlds/blasphemous/Items.py
+++ b/worlds/blasphemous/Items.py
@@ -739,6 +739,9 @@ class ItemDict(TypedDict):
"Broken Left Eye of the Traitor"}
}
+# Because each item is only in a single group, a reverse lookup table from each item to its group can be created.
+group_table_reverse: Dict[str, str] = {item: group for group, items in group_table.items() for item in items}
+
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py
index 119e618dc22f..c22857da5297 100644
--- a/worlds/blasphemous/Rules.py
+++ b/worlds/blasphemous/Rules.py
@@ -1,5 +1,6 @@
-from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING
+from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING, Mapping
from BaseClasses import CollectionState
+from worlds.generic.Rules import CollectionRule
if TYPE_CHECKING:
from . import BlasphemousWorld
@@ -7,30 +8,145 @@
BlasphemousWorld = object
+# Depending on a player's options, some logic can either always be True, or always be False.
+# When combining rules together in load_rule(), optimizations can be made by checking whether a rule being combined is
+# _always or _never.
+def _always(state: CollectionState):
+ return True
+
+
+def _never(state: CollectionState):
+ return False
+
+
+def _bool_rule(b) -> CollectionRule:
+ """Small helper to return the appropriate rule function for a rule that can be pre-calculated"""
+ if b:
+ return _always
+ else:
+ return _never
+
+
+# Player strengths required to logically beat bosses.
+# Mapping is an immutable type, so type hints should warn if attempts are made to modify it.
+BOSS_STRENGTHS: Mapping[str, float] = {
+ "warden": -0.10,
+ "ten-piedad": 0.05,
+ "charred-visage": 0.20,
+ "tres-angustias": 0.15,
+ "esdras": 0.25,
+ "melquiades": 0.25,
+ "exposito": 0.30,
+ "quirce": 0.35,
+ "crisanta": 0.50,
+ "isidora": 0.70,
+ "sierpes": 0.70,
+ "amanecida": 0.60,
+ "laudes": 0.60,
+ "perpetua": -0.05,
+ "legionary": 0.20
+}
+
+
class BlasRules:
player: int
world: BlasphemousWorld
string_rules: Dict[str, Callable[[CollectionState], bool]]
+ upwarp_skips_allowed: bool
+ mourning_skip_allowed: bool
+ enemy_skips_allowed: bool
+ obscure_skips_allowed: bool
+ precise_skips_allowed: bool
+ can_enemy_bounce: bool
+
+ # Player strengths required to logically beat bosses, adjusted by the player's difficulty option.
+ boss_strengths: Mapping[str, float]
+
+ can_enemy_upslash: CollectionRule
+ can_air_stall: CollectionRule
+ can_dawn_jump: CollectionRule
+ can_dive_laser: CollectionRule
+ can_survive_poison_1: CollectionRule
+ can_survive_poison_2: CollectionRule
+ can_survive_poison_3: CollectionRule
+
def __init__(self, world: "BlasphemousWorld") -> None:
self.player = world.player
self.world = world
self.multiworld = world.multiworld
self.indirect_conditions: List[Tuple[str, str]] = []
+ difficulty = world.options.difficulty.value
+
+ # Rules that can be fully or partially pre-calculated based on world.options.
+
+ # Special Skips
+ self.upwarp_skips_allowed = difficulty >= 2
+ self.mourning_skip_allowed = difficulty >= 2
+ self.enemy_skips_allowed = difficulty >= 2 and not world.options.enemy_randomizer.value
+ self.obscure_skips_allowed = difficulty >= 2
+ self.precise_skips_allowed = difficulty >= 2
+
+ if difficulty >= 2:
+ # Beating bosses ends up in logic earlier.
+ self.boss_strengths = {boss: strength - 0.1 for boss, strength in BOSS_STRENGTHS.items()}
+ elif difficulty >= 1:
+ self.boss_strengths = BOSS_STRENGTHS
+ else:
+ # Beating bosses ends up in logic later.
+ self.boss_strengths = {boss: strength + 0.1 for boss, strength in BOSS_STRENGTHS.items()}
+
+ # Enemy tech
+ if self.enemy_skips_allowed:
+ self.can_enemy_bounce = True
+ self.can_enemy_upslash = lambda state: self.combo(state) >= 2
+ else:
+ self.can_enemy_bounce = False
+ self.can_enemy_upslash = _never
+
+ # Movement tech
+ if difficulty >= 1:
+ self.can_air_stall = self.ranged
+ self.can_dawn_jump = lambda state: self.dawn_heart(state) and self.dash(state)
+ else:
+ self.can_air_stall = _never
+ self.can_dawn_jump = _never
+
+ # Breakable tech
+ if difficulty >= 2:
+ self.can_dive_laser = lambda state: self.dive(state) >= 3
+ else:
+ self.can_dive_laser = _never
+
+ # Lung tech
+ if difficulty >= 2:
+ self.can_survive_poison_1 = _always
+ self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
+ self.can_survive_poison_3 = lambda state: self.lung(state) or (self.tiento(state)
+ and self.total_fervour(state) >= 120)
+ elif difficulty >= 1:
+ self.can_survive_poison_1 = lambda state: self.lung(state) or self.tiento(state)
+ self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
+ self.can_survive_poison_3 = self.lung
+ else:
+ self.can_survive_poison_1 = self.lung
+ self.can_survive_poison_2 = self.lung
+ self.can_survive_poison_3 = self.lung
+
+
# BrandenEK/Blasphemous.Randomizer/ItemRando/BlasphemousInventory.cs
- self.string_rules = {
+ self.string_rules: dict[str, CollectionRule] = {
# Visibility flags
- "DoubleJump": lambda state: bool(self.world.options.purified_hand),
- "NormalLogic": lambda state: self.world.options.difficulty >= 1,
- "NormalLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 1 \
- and bool(self.world.options.purified_hand),
- "HardLogic": lambda state: self.world.options.difficulty >= 2,
- "HardLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 2 \
- and bool(self.world.options.purified_hand),
- "EnemySkips": self.enemy_skips_allowed,
- "EnemySkipsAndDoubleJump": lambda state: self.enemy_skips_allowed(state) \
- and bool(self.world.options.purified_hand),
+ "DoubleJump": _bool_rule(self.world.options.purified_hand.value),
+ "NormalLogic": _bool_rule(self.world.options.difficulty.value >= 1),
+ "NormalLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 1
+ and bool(self.world.options.purified_hand.value)),
+ "HardLogic": _bool_rule(self.world.options.difficulty.value >= 2),
+ "HardLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 2
+ and bool(self.world.options.purified_hand.value)),
+ "EnemySkips": _bool_rule(self.enemy_skips_allowed),
+ "EnemySkipsAndDoubleJump": _bool_rule(self.enemy_skips_allowed and self.world.options.purified_hand.value),
# Relics
"blood": self.blood,
@@ -52,20 +168,20 @@ def __init__(self, world: "BlasphemousWorld") -> None:
"cherubs20": lambda state: self.cherubs(state) >= 20,
"cherubs38": lambda state: self.cherubs(state) >= 38,
- "bones4": lambda state: self.bones(state) >= 4,
- "bones8": lambda state: self.bones(state) >= 8,
- "bones12": lambda state: self.bones(state) >= 12,
- "bones16": lambda state: self.bones(state) >= 16,
- "bones20": lambda state: self.bones(state) >= 20,
- "bones24": lambda state: self.bones(state) >= 24,
- "bones28": lambda state: self.bones(state) >= 28,
- "bones30": lambda state: self.bones(state) >= 30,
- "bones32": lambda state: self.bones(state) >= 32,
- "bones36": lambda state: self.bones(state) >= 36,
- "bones40": lambda state: self.bones(state) >= 40,
- "bones44": lambda state: self.bones(state) >= 44,
-
- "tears0": lambda state: True,
+ "bones4": lambda state: self.bones(state, 4),
+ "bones8": lambda state: self.bones(state, 8),
+ "bones12": lambda state: self.bones(state, 12),
+ "bones16": lambda state: self.bones(state, 16),
+ "bones20": lambda state: self.bones(state, 20),
+ "bones24": lambda state: self.bones(state, 24),
+ "bones28": lambda state: self.bones(state, 28),
+ "bones30": lambda state: self.bones(state, 30),
+ "bones32": lambda state: self.bones(state, 32),
+ "bones36": lambda state: self.bones(state, 36),
+ "bones40": lambda state: self.bones(state, 40),
+ "bones44": lambda state: self.bones(state, 44),
+
+ "tears0": _always,
# Special items
"dash": self.dash,
@@ -118,13 +234,13 @@ def __init__(self, world: "BlasphemousWorld") -> None:
# skip "dive"
# skip "lunge"
"chargeBeam": self.charge_beam,
- "rangedAttack": lambda state: self.ranged(state) > 0,
+ "rangedAttack": self.ranged,
# Main quest
- "holyWounds3": lambda state: self.holy_wounds(state) >= 3,
- "masks1": lambda state: self.masks(state) >= 1,
- "masks2": lambda state: self.masks(state) >= 2,
- "masks3": lambda state: self.masks(state) >= 3,
+ "holyWounds3": lambda state: self.holy_wounds(state, 3),
+ "masks1": lambda state: self.masks(state, 1),
+ "masks2": lambda state: self.masks(state, 2),
+ "masks3": lambda state: self.masks(state, 3),
"guiltBead": self.guilt_bead,
# LOTL quest
@@ -133,17 +249,17 @@ def __init__(self, world: "BlasphemousWorld") -> None:
"hatchedEgg": self.hatched_egg,
# Tirso quest
- "herbs1": lambda state: self.herbs(state) >= 1,
- "herbs2": lambda state: self.herbs(state) >= 2,
- "herbs3": lambda state: self.herbs(state) >= 3,
- "herbs4": lambda state: self.herbs(state) >= 4,
- "herbs5": lambda state: self.herbs(state) >= 5,
- "herbs6": lambda state: self.herbs(state) >= 6,
+ "herbs1": lambda state: self.herbs(state, 1),
+ "herbs2": lambda state: self.herbs(state, 2),
+ "herbs3": lambda state: self.herbs(state, 3),
+ "herbs4": lambda state: self.herbs(state, 4),
+ "herbs5": lambda state: self.herbs(state, 5),
+ "herbs6": lambda state: self.herbs(state, 6),
# Tentudia quest
- "tentudiaRemains1": lambda state: self.tentudia_remains(state) >= 1,
- "tentudiaRemains2": lambda state: self.tentudia_remains(state) >= 2,
- "tentudiaRemains3": lambda state: self.tentudia_remains(state) >= 3,
+ "tentudiaRemains1": lambda state: self.tentudia_remains(state, 1),
+ "tentudiaRemains2": lambda state: self.tentudia_remains(state, 2),
+ "tentudiaRemains3": lambda state: self.tentudia_remains(state, 3),
# Gemino quest
"emptyThimble": self.empty_thimble,
@@ -151,7 +267,7 @@ def __init__(self, world: "BlasphemousWorld") -> None:
"driedFlowers": self.dried_flowers,
# Altasgracias quest
- "ceremonyItems3": lambda state: self.ceremony_items(state) >= 3,
+ "ceremonyItems3": lambda state: self.ceremony_items(state, 3),
"egg": self.egg,
# Redento quest
@@ -159,13 +275,13 @@ def __init__(self, world: "BlasphemousWorld") -> None:
# skip "knots", not actually used
# Cleofas quest
- "marksOfRefuge3": lambda state: self.marks_of_refuge(state) >= 3,
+ "marksOfRefuge3": lambda state: self.marks_of_refuge(state, 3),
"cord": self.cord,
# Crisanta quest
"scapular": self.scapular,
"trueHeart": self.true_heart,
- "traitorEyes2": lambda state: self.traitor_eyes(state) >= 2,
+ "traitorEyes2": lambda state: self.traitor_eyes(state, 2),
# Jibrael quest
"bell": self.bell,
@@ -190,32 +306,32 @@ def __init__(self, world: "BlasphemousWorld") -> None:
"canSurvivePoison3": self.can_survive_poison_3,
# Enemy tech
- "canEnemyBounce": self.can_enemy_bounce,
+ "canEnemyBounce": _bool_rule(self.can_enemy_bounce),
"canEnemyUpslash": self.can_enemy_upslash,
# Reaching rooms
- "guiltRooms1": lambda state: self.guilt_rooms(state) >= 1,
- "guiltRooms2": lambda state: self.guilt_rooms(state) >= 2,
- "guiltRooms3": lambda state: self.guilt_rooms(state) >= 3,
- "guiltRooms4": lambda state: self.guilt_rooms(state) >= 4,
- "guiltRooms5": lambda state: self.guilt_rooms(state) >= 5,
- "guiltRooms6": lambda state: self.guilt_rooms(state) >= 6,
- "guiltRooms7": lambda state: self.guilt_rooms(state) >= 7,
-
- "swordRooms1": lambda state: self.sword_rooms(state) >= 1,
- "swordRooms2": lambda state: self.sword_rooms(state) >= 2,
- "swordRooms3": lambda state: self.sword_rooms(state) >= 3,
- "swordRooms4": lambda state: self.sword_rooms(state) >= 4,
- "swordRooms5": lambda state: self.sword_rooms(state) >= 5,
- "swordRooms6": lambda state: self.sword_rooms(state) >= 6,
- "swordRooms7": lambda state: self.sword_rooms(state) >= 7,
-
- "redentoRooms2": lambda state: self.redento_rooms(state) >= 2,
- "redentoRooms3": lambda state: self.redento_rooms(state) >= 3,
- "redentoRooms4": lambda state: self.redento_rooms(state) >= 4,
- "redentoRooms5": lambda state: self.redento_rooms(state) >= 5,
-
- "miriamRooms5": lambda state: self.miriam_rooms(state) >= 5,
+ "guiltRooms1": lambda state: self.guilt_rooms(state, 1),
+ "guiltRooms2": lambda state: self.guilt_rooms(state, 2),
+ "guiltRooms3": lambda state: self.guilt_rooms(state, 3),
+ "guiltRooms4": lambda state: self.guilt_rooms(state, 4),
+ "guiltRooms5": lambda state: self.guilt_rooms(state, 5),
+ "guiltRooms6": lambda state: self.guilt_rooms(state, 6),
+ "guiltRooms7": lambda state: self.guilt_rooms(state, 7),
+
+ "swordRooms1": lambda state: self.sword_rooms(state, 1),
+ "swordRooms2": lambda state: self.sword_rooms(state, 2),
+ "swordRooms3": lambda state: self.sword_rooms(state, 3),
+ "swordRooms4": lambda state: self.sword_rooms(state, 4),
+ "swordRooms5": lambda state: self.sword_rooms(state, 5),
+ "swordRooms6": lambda state: self.sword_rooms(state, 6),
+ "swordRooms7": lambda state: self.sword_rooms(state, 7),
+
+ "redentoRooms2": lambda state: self.redento_rooms(state, 2),
+ "redentoRooms3": lambda state: self.redento_rooms(state, 3),
+ "redentoRooms4": lambda state: self.redento_rooms(state, 4),
+ "redentoRooms5": lambda state: self.redento_rooms(state, 5),
+
+ "miriamRooms5": self.all_miriam_rooms,
"amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1,
"amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2,
@@ -254,11 +370,11 @@ def __init__(self, world: "BlasphemousWorld") -> None:
"openedBotSSLadder": self.opened_botss_ladder,
# Special skips
- "upwarpSkipsAllowed": self.upwarp_skips_allowed,
- "mourningSkipAllowed": self.mourning_skip_allowed,
- "enemySkipsAllowed": self.enemy_skips_allowed,
- "obscureSkipsAllowed": self.obscure_skips_allowed,
- "preciseSkipsAllowed": self.precise_skips_allowed,
+ "upwarpSkipsAllowed": _bool_rule(self.upwarp_skips_allowed),
+ "mourningSkipAllowed": _bool_rule(self.mourning_skip_allowed),
+ "enemySkipsAllowed": _bool_rule(self.enemy_skips_allowed),
+ "obscureSkipsAllowed": _bool_rule(self.obscure_skips_allowed),
+ "preciseSkipsAllowed": _bool_rule(self.precise_skips_allowed),
# Bosses
"canBeatBrotherhoodBoss": self.can_beat_brotherhood_boss,
@@ -498,30 +614,74 @@ def req_is_region(self, string: str) -> bool:
def load_rule(self, obj_is_region: bool, name: str, obj: Dict[str, Any]) -> Callable[[CollectionState], bool]:
clauses = []
+ clauses_are_impossible_if_empty = False
+ rule_indirect_conditions = []
for clause in obj["logic"]:
reqs = []
+ clause_indirect_conditions = []
+ clause_is_impossible = False
for req in clause["item_requirements"]:
if self.req_is_region(req):
if obj_is_region:
# add to indirect conditions if object and requirement are doors
- self.indirect_conditions.append((req, f"{name} -> {obj['target']}"))
+ clause_indirect_conditions.append((req, f"{name} -> {obj['target']}"))
reqs.append(lambda state, req=req: state.can_reach_region(req, self.player))
else:
+ string_rule = self.string_rules[req]
+ if string_rule is _never:
+ # This clause is not possible with the options this player has chosen.
+ clause_is_impossible = True
+ break
+ elif string_rule is _always:
+ # Don't need to add a rule that is always True with the options this player has chosen.
+ # Continue to the next requirement.
+ continue
if obj_is_region and req in self.indirect_regions:
# add to indirect conditions if object is door and requirement has list of regions
for region in self.indirect_regions[req]:
- self.indirect_conditions.append((region, f"{name} -> {obj['target']}"))
+ clause_indirect_conditions.append((region, f"{name} -> {obj['target']}"))
reqs.append(self.string_rules[req])
+ if clause_is_impossible:
+ # At least one clause was impossible, so if all clauses were impossible, the entire rule is impossible.
+ clauses_are_impossible_if_empty = True
+ # Continue to the next clause.
+ continue
+ rule_indirect_conditions.extend(clause_indirect_conditions)
+
+ # Combine the requirements if there are multiple.
+ # Requirements are AND-ed together.
if len(reqs) == 1:
clauses.append(reqs[0])
else:
- clauses.append(lambda state, reqs=reqs: all(req(state) for req in reqs))
+ def req_func(state, reqs=reqs):
+ for req in reqs:
+ if not req(state):
+ return False
+ return True
+ clauses.append(req_func)
+
+ # Combine the clauses if there are multiple.
+ # Clauses are OR-ed together.
if not clauses:
- return lambda state: True
+ # There is no need to register the indirect conditions if it turns out the rule is impossible or always
+ # possible.
+ rule_indirect_conditions.clear()
+ if clauses_are_impossible_if_empty:
+ to_return = _never
+ else:
+ to_return = _always
elif len(clauses) == 1:
- return clauses[0]
+ to_return = clauses[0]
else:
- return lambda state: any(clause(state) for clause in clauses)
+ def clause_func(state, clauses=clauses):
+ for clause in clauses:
+ if clause(state):
+ return True
+ return False
+ to_return = clause_func
+ # Update the list of indirect conditions to add.
+ self.indirect_conditions.extend(rule_indirect_conditions)
+ return to_return
# Relics
def blood(self, state: CollectionState) -> bool:
@@ -565,8 +725,10 @@ def wood_key(self, state: CollectionState) -> bool:
def cherubs(self, state: CollectionState) -> int:
return state.count("Child of Moonlight", self.player)
- def bones(self, state: CollectionState) -> int:
- return state.count_group_unique("bones", self.player)
+ def bones(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "bones" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("bones", self.player, count)
# def tears():
@@ -594,7 +756,7 @@ def dawn_heart(self, state: CollectionState) -> bool:
# Health boosts
def flasks(self, state: CollectionState) -> int:
- doors = {
+ doors = (
"D01Z05S05[SW]",
"D02Z02S04[W]",
"D03Z02S08[W]",
@@ -602,10 +764,11 @@ def flasks(self, state: CollectionState) -> int:
"D04Z02S13[W]",
"D05Z01S08[NW]",
"D20Z01S07[NE]"
- }
-
- return state.count("Empty Bile Vessel", self.player) \
- if sum(state.can_reach_region(door, self.player) for door in doors) >= 1 else 0
+ )
+ for door in doors:
+ if state.can_reach_region(door, self.player):
+ return state.count("Empty Bile Vessel", self.player)
+ return 0
def quicksilver(self, state: CollectionState) -> int:
return state.count("Quicksilver", self.player) if state.can_reach_region("D01Z05S01[W]", self.player) else 0
@@ -613,7 +776,7 @@ def quicksilver(self, state: CollectionState) -> int:
# Puzzles
def red_wax(self, state: CollectionState) -> int:
return state.count("Bead of Red Wax", self.player)
-
+
def blue_wax(self, state: CollectionState) -> int:
return state.count("Bead of Blue Wax", self.player)
@@ -670,7 +833,7 @@ def any_small_prayer(self, state: CollectionState) -> bool:
or self.cante(state)
or self.cantina(state)
or self.tiento(state)
- or state.has_any({
+ or state.has_any((
"Campanillero to the Sons of the Aurora",
"Mirabras of the Return to Port",
"Romance to the Crimson Mist",
@@ -678,7 +841,7 @@ def any_small_prayer(self, state: CollectionState) -> bool:
"Seguiriya to your Eyes like Stars",
"Verdiales of the Forsaken Hamlet",
"Zambra to the Resplendent Crown"
- }, self.player)
+ ), self.player)
)
def pillar(self, state: CollectionState) -> bool:
@@ -710,8 +873,8 @@ def combo(self, state: CollectionState) -> int:
def charged(self, state: CollectionState) -> int:
return state.count("Charged Skill", self.player)
- def ranged(self, state: CollectionState) -> int:
- return state.count("Ranged Skill", self.player)
+ def ranged(self, state: CollectionState) -> bool:
+ return state.has("Ranged Skill", self.player)
def dive(self, state: CollectionState) -> int:
return state.count("Dive Skill", self.player)
@@ -723,11 +886,15 @@ def charge_beam(self, state: CollectionState) -> bool:
return self.charged(state) >= 3
# Main quest
- def holy_wounds(self, state: CollectionState) -> int:
- return state.count_group_unique("wounds", self.player)
+ def holy_wounds(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "wounds" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("wounds", self.player, count)
- def masks(self, state: CollectionState) -> int:
- return state.count_group_unique("masks", self.player)
+ def masks(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "masks" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("masks", self.player, count)
def guilt_bead(self, state: CollectionState) -> bool:
return state.has("Weight of True Guilt", self.player)
@@ -743,12 +910,16 @@ def hatched_egg(self, state: CollectionState) -> bool:
return state.has("Hatched Egg of Deformity", self.player)
# Tirso quest
- def herbs(self, state: CollectionState) -> int:
- return state.count_group_unique("tirso", self.player)
+ def herbs(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "tirso" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("tirso", self.player, count)
# Tentudia quest
- def tentudia_remains(self, state: CollectionState) -> int:
- return state.count_group_unique("tentudia", self.player)
+ def tentudia_remains(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "tentudia" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("tentudia", self.player, count)
# Gemino quest
def empty_thimble(self, state: CollectionState) -> bool:
@@ -761,23 +932,29 @@ def dried_flowers(self, state: CollectionState) -> bool:
return state.has("Dried Flowers bathed in Tears", self.player)
# Altasgracias quest
- def ceremony_items(self, state: CollectionState) -> int:
- return state.count_group_unique("egg", self.player)
+ def ceremony_items(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "egg" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("egg", self.player, count)
def egg(self, state: CollectionState) -> bool:
return state.has("Egg of Deformity", self.player)
# Redento quest
- def limestones(self, state: CollectionState) -> int:
- return state.count_group_unique("toe", self.player)
+ def limestones(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "toe" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("toe", self.player, count)
def knots(self, state: CollectionState) -> int:
return state.count("Knot of Rosary Rope", self.player) if state.can_reach_region("D17Z01S07[NW]", self.player)\
else 0
# Cleofas quest
- def marks_of_refuge(self, state: CollectionState) -> int:
- return state.count_group_unique("marks", self.player)
+ def marks_of_refuge(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "marks" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("marks", self.player, count)
def cord(self, state: CollectionState) -> bool:
return state.has("Cord of the True Burying", self.player)
@@ -789,8 +966,10 @@ def scapular(self, state: CollectionState) -> bool:
def true_heart(self, state: CollectionState) -> bool:
return state.has("Apodictic Heart of Mea Culpa", self.player)
- def traitor_eyes(self, state: CollectionState) -> int:
- return state.count_group_unique("eye", self.player)
+ def traitor_eyes(self, state: CollectionState, count: int) -> bool:
+ # Count of unique items in the "eye" item group that have been collected into state.
+ # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
+ return state.has("eye", self.player, count)
# Jibrael quest
def bell(self, state: CollectionState) -> bool:
@@ -800,19 +979,6 @@ def verses(self, state: CollectionState) -> int:
return state.count("Verses Spun from Gold", self.player)
# Movement tech
- def can_air_stall(self, state: CollectionState) -> bool:
- return (
- self.ranged(state) > 0
- and self.world.options.difficulty >= 1
- )
-
- def can_dawn_jump(self, state: CollectionState) -> bool:
- return (
- self.dawn_heart(state)
- and self.dash(state)
- and self.world.options.difficulty >= 1
- )
-
def can_water_jump(self, state: CollectionState) -> bool:
return (
self.nail(state)
@@ -828,12 +994,6 @@ def can_break_holes(self, state: CollectionState) -> bool:
or self.can_use_any_prayer(state)
)
- def can_dive_laser(self, state: CollectionState) -> bool:
- return (
- self.dive(state) >= 3
- and self.world.options.difficulty >= 2
- )
-
# Root tech
def can_walk_on_root(self, state: CollectionState) -> bool:
return self.root(state)
@@ -844,40 +1004,6 @@ def can_climb_on_root(self, state: CollectionState) -> bool:
and self.wall_climb(state)
)
- # Lung tech
- def can_survive_poison_1(self, state: CollectionState) -> bool:
- return (
- self.lung(state)
- or self.world.options.difficulty >= 1
- and self.tiento(state)
- or self.world.options.difficulty >= 2
- )
-
- def can_survive_poison_2(self, state: CollectionState) -> bool:
- return (
- self.lung(state)
- or self.world.options.difficulty >= 1
- and self.tiento(state)
- )
-
- def can_survive_poison_3(self, state: CollectionState) -> bool:
- return (
- self.lung(state)
- or self.world.options.difficulty >= 2
- and self.tiento(state)
- and self.total_fervour(state) >= 120
- )
-
- # Enemy tech
- def can_enemy_bounce(self, state: CollectionState) -> bool:
- return self.enemy_skips_allowed(state)
-
- def can_enemy_upslash(self, state: CollectionState) -> bool:
- return (
- self.combo(state) >= 2
- and self.enemy_skips_allowed(state)
- )
-
# Crossing gaps
def can_cross_gap_1(self, state: CollectionState) -> bool:
return (
@@ -1021,7 +1147,7 @@ def broke_jondo_bell_e(self, state: CollectionState) -> bool:
or state.can_reach_region("D03Z02S03[E]", self.player)
and (
self.can_cross_gap_5(state)
- or self.can_enemy_bounce(state)
+ or self.can_enemy_bounce
and self.can_cross_gap_3(state)
)
)
@@ -1067,25 +1193,6 @@ def opened_botss_ladder(self, state: CollectionState) -> bool:
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
)
- # Special skips
- def upwarp_skips_allowed(self, state: CollectionState) -> bool:
- return self.world.options.difficulty >= 2
-
- def mourning_skip_allowed(self, state: CollectionState) -> bool:
- return self.world.options.difficulty >= 2
-
- def enemy_skips_allowed(self, state: CollectionState) -> bool:
- return (
- self.world.options.difficulty >= 2
- and not self.world.options.enemy_randomizer
- )
-
- def obscure_skips_allowed(self, state: CollectionState) -> bool:
- return self.world.options.difficulty >= 2
-
- def precise_skips_allowed(self, state: CollectionState) -> bool:
- return self.world.options.difficulty >= 2
-
# Bosses
def can_beat_brotherhood_boss(self, state: CollectionState) -> bool:
return (
@@ -1183,18 +1290,18 @@ def can_beat_mourning_boss(self, state: CollectionState) -> bool:
and state.can_reach_region("D20Z02S07[W]", self.player)
)
- def can_beat_graveyard_boss(self, state: CollectionState) -> bool:
+ def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
- self.has_boss_strength(state, "amanecida")
+ self.has_boss_strength(state, "amanecida", player_strength)
and self.wall_climb(state)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D02Z03S18[NW]", self.player)
and state.can_reach_region("D02Z02S03[NE]", self.player)
)
- def can_beat_jondo_boss(self, state: CollectionState) -> bool:
+ def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
- self.has_boss_strength(state, "amanecida")
+ self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and (
state.can_reach_region("D20Z01S06[NE]", self.player)
@@ -1206,9 +1313,9 @@ def can_beat_jondo_boss(self, state: CollectionState) -> bool:
)
)
- def can_beat_patio_boss(self, state: CollectionState) -> bool:
+ def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
- self.has_boss_strength(state, "amanecida")
+ self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D06Z01S02[W]", self.player)
and (
@@ -1218,9 +1325,9 @@ def can_beat_patio_boss(self, state: CollectionState) -> bool:
)
)
- def can_beat_wall_boss(self, state: CollectionState) -> bool:
+ def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
- self.has_boss_strength(state, "amanecida")
+ self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D09Z01S09[Cell24]", self.player)
and (
@@ -1244,8 +1351,7 @@ def can_beat_perpetua(self, state: CollectionState) -> bool:
def can_beat_legionary(self, state: CollectionState) -> bool:
return self.has_boss_strength(state, "legionary")
-
- def has_boss_strength(self, state: CollectionState, boss: str) -> bool:
+ def get_player_strength(self, state: CollectionState) -> float:
life: int = state.count("Life Upgrade", self.player)
sword: int = state.count("Mea Culpa Upgrade", self.player)
fervour: int = state.count("Fervour Upgrade", self.player)
@@ -1259,30 +1365,16 @@ def has_boss_strength(self, state: CollectionState, boss: str) -> bool:
+ min(8, flasks) * 0.15 / 8
+ min(5, quicksilver) * 0.15 / 5
)
+ return player_strength
- bosses: Dict[str, float] = {
- "warden": -0.10,
- "ten-piedad": 0.05,
- "charred-visage": 0.20,
- "tres-angustias": 0.15,
- "esdras": 0.25,
- "melquiades": 0.25,
- "exposito": 0.30,
- "quirce": 0.35,
- "crisanta": 0.50,
- "isidora": 0.70,
- "sierpes": 0.70,
- "amanecida": 0.60,
- "laudes": 0.60,
- "perpetua": -0.05,
- "legionary": 0.20
- }
- boss_strength: float = bosses[boss]
- return player_strength >= (boss_strength - 0.10 if self.world.options.difficulty >= 2 else
- (boss_strength if self.world.options.difficulty >= 1 else boss_strength + 0.10))
+ def has_boss_strength(self, state: CollectionState, boss: str, player_strength: float | None = None) -> bool:
+ if player_strength is None:
+ return self.get_player_strength(state) >= self.boss_strengths[boss]
+ else:
+ return player_strength >= self.boss_strengths[boss]
- def guilt_rooms(self, state: CollectionState) -> int:
- doors = [
+ def guilt_rooms(self, state: CollectionState, count: int) -> bool:
+ doors = (
"D01Z04S01[NE]",
"D02Z02S11[W]",
"D03Z03S02[NE]",
@@ -1290,20 +1382,25 @@ def guilt_rooms(self, state: CollectionState) -> int:
"D05Z01S05[NE]",
"D09Z01S05[W]",
"D17Z01S04[W]",
- ]
+ )
- return sum(state.can_reach_region(door, self.player) for door in doors)
-
- def sword_rooms(self, state: CollectionState) -> int:
- doors = [
- ["D01Z02S07[E]", "D01Z02S02[SW]"],
- ["D20Z01S04[E]", "D01Z05S23[W]"],
- ["D02Z03S02[NE]"],
- ["D04Z02S21[NE]"],
- ["D05Z01S21[NW]"],
- ["D06Z01S15[NE]"],
- ["D17Z01S07[SW]"]
- ]
+ total: int = 0
+ for door in doors:
+ total += state.can_reach_region(door, self.player)
+ if total >= count:
+ return True
+ return False
+
+ def sword_rooms(self, state: CollectionState, count: int) -> bool:
+ doors = (
+ ("D01Z02S07[E]", "D01Z02S02[SW]"),
+ ("D20Z01S04[E]", "D01Z05S23[W]"),
+ ("D02Z03S02[NE]",),
+ ("D04Z02S21[NE]",),
+ ("D05Z01S21[NW]",),
+ ("D06Z01S15[NE]",),
+ ("D17Z01S07[SW]",)
+ )
total: int = 0
for subdoors in doors:
@@ -1311,72 +1408,90 @@ def sword_rooms(self, state: CollectionState) -> int:
if state.can_reach_region(door, self.player):
total += 1
break
+ if total >= count:
+ return True
- return total
+ return False
- def redento_rooms(self, state: CollectionState) -> int:
- if (
- state.can_reach_region("D03Z01S04[E]", self.player)
- or state.can_reach_region("D03Z02S10[N]", self.player)
+ def redento_rooms(self, state: CollectionState, count: int) -> bool:
+ if not (
+ state.can_reach_region("D03Z01S04[E]", self.player)
+ or state.can_reach_region("D03Z02S10[N]", self.player)
):
- if (
+ # Realistically, count should never be zero or negative.
+ return count < 1
+
+ if count == 1:
+ return True
+
+ if not (
state.can_reach_region("D17Z01S05[S]", self.player)
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
- ):
- if (
- state.can_reach_region("D01Z03S04[E]", self.player)
- or state.can_reach_region("D08Z01S01[W]", self.player)
- ):
- if (
- state.can_reach_region("D04Z01S03[E]", self.player)
- or state.can_reach_region("D04Z02S01[W]", self.player)
- or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)
- ):
- if (
- self.knots(state) >= 1
- and self.limestones(state) >= 3
- and (
- state.can_reach_region("D04Z02S08[E]", self.player)
- or state.can_reach_region("D04BZ02S01[Redento]", self.player)
- )
- ):
- return 5
- return 4
- return 3
- return 2
- return 1
- return 0
-
- def miriam_rooms(self, state: CollectionState) -> int:
- doors = [
+ ):
+ return False
+
+ if count == 2:
+ return True
+
+ if not (state.can_reach_region("D01Z03S04[E]", self.player)
+ or state.can_reach_region("D08Z01S01[W]", self.player)):
+ return False
+
+ if count == 3:
+ return True
+
+ if not (state.can_reach_region("D04Z01S03[E]", self.player)
+ or state.can_reach_region("D04Z02S01[W]", self.player)
+ or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)):
+ return False
+
+ if count == 4:
+ return True
+
+ if not (
+ self.knots(state) >= 1
+ and self.limestones(state, 3)
+ and (state.can_reach_region("D04Z02S08[E]", self.player)
+ or state.can_reach_region("D04BZ02S01[Redento]", self.player))
+ ):
+ return False
+
+ return count == 5
+
+ def all_miriam_rooms(self, state: CollectionState) -> bool:
+ doors = (
"D02Z03S07[NWW]",
"D03Z03S07[NW]",
"D04Z04S01[E]",
"D05Z01S06[W]",
"D06Z01S17[E]"
- ]
+ )
- return sum(state.can_reach_region(door, self.player) for door in doors)
+ for door in doors:
+ if not state.can_reach_region(door, self.player):
+ return False
+ return True
def amanecida_rooms(self, state: CollectionState) -> int:
+ player_strength = self.get_player_strength(state)
total: int = 0
- if self.can_beat_graveyard_boss(state):
+ if self.can_beat_graveyard_boss(state, player_strength):
total += 1
- if self.can_beat_jondo_boss(state):
+ if self.can_beat_jondo_boss(state, player_strength):
total += 1
- if self.can_beat_patio_boss(state):
+ if self.can_beat_patio_boss(state, player_strength):
total += 1
- if self.can_beat_wall_boss(state):
+ if self.can_beat_wall_boss(state, player_strength):
total += 1
return total
def chalice_rooms(self, state: CollectionState) -> int:
- doors = [
- ["D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"],
- ["D05Z01S11[SE]", "D05Z02S02[NW]"],
- ["D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]"]
- ]
+ doors = (
+ ("D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"),
+ ("D05Z01S11[SE]", "D05Z02S02[NW]"),
+ ("D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]")
+ )
total: int = 0
for subdoors in doors:
diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py
index 9dffc6c6d286..42ed49dbe071 100644
--- a/worlds/blasphemous/__init__.py
+++ b/worlds/blasphemous/__init__.py
@@ -1,9 +1,9 @@
from typing import Dict, List, Set, Any
from collections import Counter
-from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
+from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, CollectionState
from Options import OptionError
from worlds.AutoWorld import World, WebWorld
-from .Items import base_id, item_table, group_table, tears_list, reliquary_set
+from .Items import base_id, item_table, group_table, tears_list, reliquary_set, group_table_reverse
from .Locations import location_names
from .Rules import BlasRules
from worlds.generic.Rules import set_rule
@@ -216,6 +216,27 @@ def place_items_from_dict(self, option_dict: Dict[str, str]):
for loc, item in option_dict.items():
self.get_location(loc).place_locked_item(self.create_item(item))
+ def collect(self, state: CollectionState, item: Item) -> bool:
+ changed = super().collect(state, item)
+ if changed:
+ name = item.name
+ if name in group_table_reverse and state.count(name, self.player) == 1:
+ # Count was 0 before super().collect().
+ group_name = group_table_reverse[name]
+ # Increase unique count for items in this group.
+ state.prog_items[self.player][group_name] += 1
+ return changed
+
+ def remove(self, state: CollectionState, item: Item) -> bool:
+ changed = super().remove(state, item)
+ if changed:
+ name = item.name
+ if name in group_table_reverse and state.count(name, self.player) == 0:
+ # Count was 1 before super().remove().
+ group_name = group_table_reverse[name]
+ # Decrease unique count for items in this group.
+ state.prog_items[self.player][group_name] -= 1
+ return changed
def create_regions(self) -> None:
multiworld = self.multiworld
diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py
index 52222e4d3981..afe1dd450a62 100644
--- a/worlds/dark_souls_3/Options.py
+++ b/worlds/dark_souls_3/Options.py
@@ -296,11 +296,9 @@ class ImpatientMimicsOption(Toggle):
class RandomEnemyPresetOption(OptionDict):
"""The YAML preset for the static enemy randomizer.
- See the static randomizer documentation in `randomizer\\presets\\README.txt` for details.
+ See the online enemy randomization documentation for all available options.
Include this as nested YAML. For example:
- .. code-block:: YAML
-
random_enemy_preset:
RemoveSource: Ancient Wyvern; Darkeater Midir
DontRandomize: Iudex Gundyr
diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md
index 06227226aafe..ea7934a6981f 100644
--- a/worlds/dark_souls_3/docs/en_Dark Souls III.md
+++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md
@@ -1,9 +1,11 @@
# Dark Souls III
-Game Page | [Items] | [Locations]
+Game Page | [Setup] | [Items] | [Locations] | [Enemy Randomization]
+[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
+[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## What do I need to do to randomize DS3?
@@ -138,6 +140,14 @@ Check out the [item guide], which explains the named groups available for items.
[item guide]: /tutorial/Dark%20Souls%20III/items/en
+## How can I change what enemies get randomized?
+
+The [enemy randomization guide] explains how to further customize enemy randomization
+for challenge runs or convenience. You can target specific enemies or entire
+categories and even remove annoying enemy types outright.
+
+[enemy randomization guide]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
+
## What's new from 2.x.x?
Version 3.0.0 of the Dark Souls III Archipelago client has a number of
diff --git a/worlds/dark_souls_3/docs/enemy-randomization_en.md b/worlds/dark_souls_3/docs/enemy-randomization_en.md
new file mode 100644
index 000000000000..dd545872250f
--- /dev/null
+++ b/worlds/dark_souls_3/docs/enemy-randomization_en.md
@@ -0,0 +1,411 @@
+# Dark Souls III Enemy Randomization
+
+[Game Page] | [Setup] | [Items] | [Locations] | Enemy Randomization
+
+[Game Page]: /games/Dark%20Souls%20III/info/en
+[Setup]: /tutorial/Dark%20Souls%20III/setup/en
+[Items]: /tutorial/Dark%20Souls%20III/items/en
+[Locations]: /tutorial/Dark%20Souls%20III/locations/en
+
+If `randomize_enemies` in your Dark Souls 3 player config YAML is enabled, bosses, minibosses and basic enemies will
+be shuffled with themselves respectively.
+
+To further customize enemy randomization beyond that, there is a section called `random_enemy_preset`.
+
+This tutorial will show all the ways how to configure that preset.
+
+## Table of Contents
+- [The Basics](#the-basics)
+- [Individual Assignments](#individual-assignments)
+- [Pools](#pools)
+ * [Pool Groups](#pool-groups)
+ + [RandomByType](#randombytype)
+ * [Weights](#weights)
+- [Settings](#settings)
+ * [Boss](#boss)
+ * [Miniboss](#miniboss)
+ * [Basic](#basic)
+ + [BuffBasicEnemiesAsBosses](#buffbasicenemiesasbosses)
+ * [Enemies](#enemies)
+ * [DontRandomize](#dontrandomize)
+ * [RemoveSource](#removesource)
+ * [OopsAll](#oopsall)
+- [Enemy Categories](#enemy-categories)
+
+## The Basics
+
+There are two main ways to assign an enemy to be randomized: [individual enemy assignments](#individual-assignments)
+to target a singular enemy placement and setting up [Pools](#pools) to target a category of enemies.
+
+Custom pools are recommended unless you specifically want to single out one enemy placement.
+
+All bosses also have their own category, so individual assignment is not necessary in those cases.
+
+Be aware of correct indentation of your YAML file. Every example in this document will need to be nested under the
+`random_enemy_preset:` section.
+
+Disable the preset by leaving just empty brackets `{}`. Like usual with YAML, you can add comments by using `#`.
+
+For further examples, check out the "presets" folder of the standalone randomizer.
+
+## Individual Assignments
+
+Individual enemy assignment allows you to target individual enemies, rather than a category as under pools.
+
+This overrides pools and any other configuration, will usually ignore progression, and can possibly cause you to have to
+fight Yhorm the Giant without Storm Ruler.
+
+You use it in the [`Enemies`](#enemies) section by selecting a specific enemy using its unique
+ID, or its specific name followed by its ID.
+
+See the '/randomizer/preset/Template.txt' file of the static randomizer for all available IDs.
+
+There are also some special target names available for individual assignments:
+
+- `any`: This is the default and allows any enemy in the pool to appear there.
+
+- `norandom`: Assigns an enemy to itself. This has the same effect as adding the enemy name to [`DontRandomize`](#dontrandomize).
+
+## Pools
+
+A pool is a collection of enemies. A pool can both be a randomization target and an eligible group of random enemies to
+be drawn from for randomization. See [Enemy Categories](#enemy-categories) for all available pools.
+
+Pool assignment generally respects progression, like requiring Storm Ruler to be accessible before Yhorm the Giant.
+
+By default, using a boss as another boss, or a miniboss as another miniboss, takes the source enemy out of the default
+pool for that category, so each enemy will still be used once if possible. However, the enemy can still appear more
+than once if used in a custom pool.
+
+### Pool Groups
+
+Pools can be joined into a pool group by joining several names, separated by a semicolon.
+
+```yaml
+# All basic enemies are just different hollows now
+Basic:
+- Weight: 100
+ Pool: Hollow Soldiers; Large Hollow Soldiers
+```
+
+#### RandomByType
+
+By default, selection will be random across all eligible enemies. In our example above it would select from:
+
+- Hollow Soldier
+- Road of Sacrifices Hollow Soldier
+- Cathedral Hollow Soldier
+- Lothric Castle Hollow Soldier
+- Grand Archives Hollow Soldier
+
+and
+
+- Large Hollow Soldier
+- Cathedral Large Hollow Soldier
+- Lothric Castle Large Hollow Soldier
+
+However, this would make it more likely to select a regular soldier instead of a large one (5 out of 8), just because
+there are fewer entries in the latter category.
+
+You can specify `RandomByType: true` to select randomly from the list itself (Hollow Soldiers, Large Hollow Soldiers)
+and make our previous example a true 50/50 split.
+
+```yaml
+# All basic enemies are just different hollows now
+Basic:
+- Weight: 100
+ Pool: Hollow Soldiers; Large Hollow Soldiers
+ RandomByType: true # To make it truly 50/50 between the categories
+```
+
+### Weights
+
+Weights can be used to select multiple different outcomes within a pool, weighted to give different probabilities each.
+
+Weights don't necessarily have to add up to 100, but doing it that way makes estimating probabilities very intuitive.
+
+```yaml
+Boss:
+- Weight: 79 # 79% of bosses will still be bosses
+ Pool: default
+- Weight: 20 # Replace 20% of all bosses with minibosses
+ Pool: Miniboss
+- Weight: 1 # Replace 1% of all bosses with regular enemies. It's always funny
+ Pool: Basic
+```
+
+Be aware that weights will not work in the [`Enemies`](#enemies) section.
+
+## Settings
+
+### Boss
+
+This setting indicates which enemies can be used as replacements for bosses.
+By default, this is the pool of all 29 bosses.
+
+```yaml
+Boss:
+- Weight: 80
+ Pool: default
+- Weight: 20 # Replace 20% of all bosses with minibosses
+ Pool: Miniboss
+```
+
+### Miniboss
+
+This setting indicates which enemies can be used as replacements for minibosses.
+By default, this is the pool of all 32 minibosses (including duplicates).
+
+```yaml
+Miniboss:
+- Weight: 80
+ Pool: default
+- Weight: 20 # Replace 20% of all minibosses with bosses
+ Pool: Boss
+```
+
+### Basic
+
+This setting indicates which enemies can be used as replacements for all other enemies, so non-bosses and non-minibosses.
+By default, this is the pool of all ~2000 basic enemies (including duplicates).
+
+```yaml
+Basic:
+- Weight: 94
+ Pool: default
+- Weight: 5 # Replace 5% of all basic enemies with minibosses
+ Pool: Miniboss
+- Weight: 1 # Replace 1% of all basic enemies with bosses
+ Pool: Boss
+```
+
+#### BuffBasicEnemiesAsBosses
+
+If enabled, this causes basic enemies to become a lot stronger when randomized into the slot of a boss.
+
+```yaml
+Boss:
+- Weight: 100 # All bosses are just basic enemies...
+ Pool: Basic
+
+BuffBasicEnemiesAsBosses: true # ...but they are strong
+```
+
+### Enemies
+
+Under the `Enemies:` setting you can add more nuanced replacements of random enemies.
+There are two ways you can adjust enemies:
+- Assign to a group of enemies using their category pool (see [Enemy Categories](#enemy-categories))
+- Assign to one specifc enemy by using its number (see [Individual Assignments](#individual-assignments))
+
+```yaml
+Enemies:
+ # Replace only the very first Ravenous Crystal Lizard with the final boss
+ Ravenous Crystal Lizard 4000380: Lords of Cinder
+
+ # Replace all regular soldiers with skeletons or small crabs
+ Hollow Soldiers: Skeletons; Lesser Crab
+
+ # Knights remain knights, but variants (i.e. weapons) are still shuffled within the category
+ High Wall Lothric Knight: High Wall Lothric Knight
+```
+
+### DontRandomize
+
+A semicolon-separated list of enemies or enemy types to not randomize (assign to themselves).
+It is taken out of its default pool and also custom pools in this case, but it can still be assigned to
+[individual enemies](#individual-assignments).
+
+```yaml
+DontRandomize: Iudex Gundyr # Iudex Gundyr will be at his vanilla location
+
+Boss:
+- Weight: 100
+ Pool: default # Boss slots other than Iudex Gundyr will never become him
+```
+
+### RemoveSource
+
+A semicolon-separated list of enemies or enemy types to remove from all pools.
+It can still be assigned to individual enemies.
+This is overridden by [`DontRandomize`](#dontrandomize) directives.
+
+```yaml
+# Remove the most annoying enemies from all pools
+RemoveSource: Bridge Darkeater Midir; Ancient Wyvern Mob; Curse-rotted Greatwood; High Lord Wolnir; Carthus Sandworm
+```
+
+### OopsAll
+
+Assigning an enemy or a pool to `OopsAll` sets all pools to that specific enemy or category of enemy. This can still be
+overridden using [individual enemy assginments](#individual-assignments), but otherwise every enemy is replaced by
+this setting.
+
+```yaml
+# This run suddenly got very spooky
+OopsAll: Skeletons
+```
+
+---
+
+## Enemy Categories
+
+The following enemy category pools are available:
+
+- Any
+- Bosses
+- Minibosses
+- Bosses and Minibosses
+- Basic
+- Abyss Watchers
+- Aldrich, Devourer of Gods
+- Ancient Wyvern
+- Ancient Wyvern Mob
+- Angel Pilgrim
+- Basilisk
+- Black Knight
+- Blackflame Friede
+- Boreal Outrider Knight
+- Bridge Darkeater Midir
+- Cage Spider
+- Carthus Sandworm
+- Cathedral Evangelist
+- Cathedral Knight
+- Cemetery Hollow
+- Champion Gundyr
+- Champion's Gravetender and Gravetender Greatwolf
+- Consumed King Oceiros
+- Corpse-grub
+- Corvian
+- Corvian Knight
+- Corvian Settler
+- Crabs
+ - Lesser Crab
+ - Greater Crab
+ - Ariandel Greater Crab
+- Crystal Lizard
+- Crystal Sage
+- Crystal Sage in Archives
+- Curse-rotted Greatwood
+- Dancer of the Boreal Valley
+- Darkeater Midir
+- Darkwraith
+- Deacon
+ - Cathedral Deacon
+ - Wide Deacon
+ - Irirthyll Deacon
+ - Irirthyll Tall Deacon
+- Deacons of the Deep
+- Deep Accursed
+- Demon
+- Demon Cleric
+- Demon Prince
+- Demonic Statue
+- Dragonslayer Armour
+- Dreg Heap Thrall
+- Elder Ghru
+- Father Ariandel
+- Farron Follower
+- Fire Witch
+- Gargoyles
+ - Profaned Capital Gargoyle
+ - Archives Gargoyle
+- Ghru Grunt
+- Giant Fly
+- Giant Slave
+- Grand Archives Scholar
+- Grave Warden
+- Halflight, Spear of the Church
+- Harald Legion Knight
+- High Lord Wolnir
+- Hobbled Cleric
+- Hollow Manservant
+- Hollow Soldiers
+ - Hollow Soldier
+ - Road of Sacrifices Hollow Soldier
+ - Cathedral Hollow Soldier
+ - Lothric Castle Hollow Soldier
+ - Grand Archives Hollow Soldier
+- Hound Rat
+- Infested Corpse
+- Irirthyll Dungeon Peasant Hollow
+- Irithyll Giant Slave
+- Irithyll Starved Hound
+- Irithyllian Slave
+- Iudex Gundyr
+- Jailer
+- Judicator
+- King of the Storm
+- Large Hollow Soldiers
+ - Large Hollow Soldier
+ - Cathedral Large Hollow Soldier
+ - Lothric Castle Large Hollow Soldier
+- Large Hound Rat
+- Large Serpent-Man
+- Large Starved Hound
+- Locust Preacher
+- Lords of Cinder (actually called "Soul of Cinder" ingame)
+- Lorian, Elder Prince
+- Lothric Knights
+ - High Wall Lothric Knight
+ - Lothric Castle Lothric Knight
+ - Dreg Heap Lothric Knight
+ - Red-Eyed Lothric Knight
+- Lothric Priest
+- Lothric, Younger Prince
+- Lycanthrope
+- Lycanthrope Hunter
+- Maggot Belly Starved Hound
+- Millwood Knight
+- Mimic Chest
+- Monstrosity of Sin
+- Murkman
+- Murkman Summoner
+- Nameless King
+- Old Demon King
+- Passive Locust Preacher
+- Peasant Hollow
+- Poisonhorn Bug
+- Pontiff Knight
+- Pontiff Sulyvahn
+- Pus of Man
+- Ravenous Crystal Lizard
+- Reanimated Corpse
+- Ringed City Cleric
+- Ringed Knight
+- Road of Sacrifices Sorcerer
+- Rock Lizard
+- Rotten Slug
+- Serpent-Man
+- Serpent-Man Summoner
+- Sewer Centipede
+- Silver Knight
+- Sister Friede
+- Skeletons
+ - Skeleton
+ - Bonewheel Skeleton
+ - Carthus Curved Sword Skeleton
+ - Carthus Shotel Skeleton
+ - Ringed City Skeleton
+- Slave Knight Gael
+ - Slave Knight Gael 1
+ - Slave Knight Gael 2
+- Small Locust Preacher
+- Smouldering Ghru Grunt
+- Starved Hound
+- Stray Demon
+- Sulyvahn's Beast
+- Thrall
+- Tree Woman
+- Vordt of the Boreal Valley
+- Winged Knight
+- Wolves
+ - Smaller Wolf
+ - Larger Wolf
+ - Greatwolf
+- Wretch
+- Writhing Flesh
+ - Catacombs Writhing Flesh
+ - Smouldering Writhing Flesh
+ - Anor Londo Writhing Flesh
+- Yhorm the Giant
diff --git a/worlds/dark_souls_3/docs/items_en.md b/worlds/dark_souls_3/docs/items_en.md
index b9de5e500a96..88557faeab1a 100644
--- a/worlds/dark_souls_3/docs/items_en.md
+++ b/worlds/dark_souls_3/docs/items_en.md
@@ -1,9 +1,11 @@
# Dark Souls III Items
-[Game Page] | Items | [Locations]
+[Game Page] | [Setup] | Items | [Locations] | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
+[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
+[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Item Groups
diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md
index 4f0160a96d99..71deabe579d6 100644
--- a/worlds/dark_souls_3/docs/locations_en.md
+++ b/worlds/dark_souls_3/docs/locations_en.md
@@ -1,9 +1,11 @@
# Dark Souls III Locations
-[Game Page] | [Items] | Locations
+[Game Page] | [Setup] | [Items] | Locations | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
+[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
+[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Table of Contents
diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md
index 7edf0d54e101..a72bf19dc25f 100644
--- a/worlds/dark_souls_3/docs/setup_en.md
+++ b/worlds/dark_souls_3/docs/setup_en.md
@@ -1,5 +1,12 @@
# Dark Souls III Randomizer Setup Guide
+[Game Page] | Setup | [Items] | [Locations] | [Enemy Randomization]
+
+[Game Page]: /games/Dark%20Souls%20III/info/en
+[Items]: /tutorial/Dark%20Souls%20III/items/en
+[Locations]: /tutorial/Dark%20Souls%20III/locations/en
+[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
+
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py
old mode 100755
new mode 100644
index beec7c434e93..f6b28f2ceb2a
--- a/worlds/factorio/Client.py
+++ b/worlds/factorio/Client.py
@@ -19,7 +19,7 @@
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
-from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between
+from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between, user_path
from .settings import FactorioSettings
from settings import get_settings
@@ -63,7 +63,7 @@ def _cmd_toggle_connection_change_filter(self):
def _cmd_toggle_chat(self):
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
self.ctx.toggle_bridge_chat_out()
-
+
def _cmd_rcon_reconnect(self) -> bool:
"""Reconnect the RCON client if its disconnected."""
try:
@@ -88,7 +88,7 @@ class FactorioContext(CommonContext):
def __init__(self, server_address, password, filter_connection_changes: bool, filter_item_sends: bool, bridge_chat_out: bool,
rcon_port: int, rcon_password: str, server_settings_path: str | None,
- factorio_server_args: tuple[str, ...]):
+ config_file: str, factorio_server_args: tuple[str, ...] | list[str]):
super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0
self.rcon_client = None
@@ -105,6 +105,7 @@ def __init__(self, server_address, password, filter_connection_changes: bool, fi
self.rcon_port: int = rcon_port
self.rcon_password: str = rcon_password
self.server_settings_path: str = server_settings_path
+ self.config_file: str = config_file
self.additional_factorio_server_args = factorio_server_args
@property
@@ -158,9 +159,11 @@ def server_args(self) -> tuple[str, ...]:
"--rcon-port", str(self.rcon_port),
"--rcon-password", self.rcon_password,
"--server-settings", self.server_settings_path,
+ "--config", self.config_file,
*self.additional_factorio_server_args)
else:
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
+ "--config", self.config_file,
*self.additional_factorio_server_args)
@property
@@ -364,7 +367,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
- executable, "--create", savegame_name, "--preset", "archipelago"
+ executable, "--create", savegame_name, "--preset", "archipelago", "--config", ctx.config_file
))
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
*ctx.server_args),
@@ -474,11 +477,11 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
- savegame_name = os.path.abspath("Archipelago.zip")
+ savegame_name = user_path("factorio", "saves", "Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
- executable, "--create", savegame_name
+ executable, "--create", savegame_name, "--config", ctx.config_file
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *ctx.server_args),
@@ -609,6 +612,9 @@ def launch(*new_args: str):
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
+ if os.path.isdir(executable) and os.path.exists(os.path.join(executable, "Contents", "MacOS", "factorio")):
+ # user entered the .App bundle, let's find the executable
+ executable = os.path.join(executable, "Contents", "MacOS", "factorio")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
@@ -617,9 +623,15 @@ def launch(*new_args: str):
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
+ config_file = user_path('factorio', 'config', 'apconfig.ini')
+ if not os.path.exists(config_file):
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
+ with open(config_file, 'w') as f:
+ f.write(f"[path]\nread-data=__PATH__system-read-data__\nwrite-data={user_path('factorio')}")
+
asyncio.run(main(lambda: FactorioContext(
args.connect, args.password,
initial_filter_connection_changes, initial_filter_item_sends, initial_bridge_chat_out,
- rcon_port, rcon_password, server_settings, rest
+ rcon_port, rcon_password, server_settings, config_file, rest
)))
colorama.deinit()
diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py
index 00ef0d866a30..b53b0fa05f87 100644
--- a/worlds/factorio/Mod.py
+++ b/worlds/factorio/Mod.py
@@ -23,7 +23,6 @@
data_template: Optional[jinja2.Template] = None
data_final_template: Optional[jinja2.Template] = None
-locale_template: Optional[jinja2.Template] = None
control_template: Optional[jinja2.Template] = None
settings_template: Optional[jinja2.Template] = None
@@ -94,7 +93,7 @@ def generate_mod(world: "Factorio", output_directory: str):
multiworld = world.multiworld
random = world.random
- global data_final_template, locale_template, control_template, data_template, settings_template
+ global data_final_template, control_template, data_template, settings_template
with template_load_lock:
if not data_final_template:
def load_template(name: str):
@@ -107,7 +106,6 @@ def load_template(name: str):
data_template = template_env.get_template("data.lua")
data_final_template = template_env.get_template("data-final-fixes.lua")
- locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua")
settings_template = template_env.get_template("settings.lua")
# get data for templates
@@ -195,9 +193,7 @@ def flop_random(low, high, base=None):
control_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua",
settings_template.render(**template_data)))
- mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg",
- locale_template.render(**template_data)))
-
+
info = base_info.copy()
info["name"] = mod_name
mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json",
diff --git a/worlds/factorio/data/mod/locale/en/locale.cfg b/worlds/factorio/data/mod/locale/en/locale.cfg
new file mode 100644
index 000000000000..7d60d90b4398
--- /dev/null
+++ b/worlds/factorio/data/mod/locale/en/locale.cfg
@@ -0,0 +1,36 @@
+[map-gen-preset-name]
+archipelago=Archipelago
+
+[entity-name]
+ap-energy-bridge=Archipelago EnergyLink Bridge
+
+[map-gen-preset-description]
+archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
+
+[technology-name]
+ap-technology-full=__1__'s __2__ (__3__)
+ap-technology-hidden=__1__
+
+[technology-description]
+ap-technology-full=Researching this technology sends __1__ to __2____3__.
+ap-technology-item-advancement=, which is considered a logical advancement
+ap-technology-item-useful=, which is considered useful
+ap-technology-item-trap=, which is considered fun
+ap-technology-hidden=Researching this technology sends something to someone__1__.
+
+[mod-setting-name]
+archipelago-death-link=Death Link
+
+[mod-setting-description]
+archipelago-death-link=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
+
+[archipelago]
+receive-ap-item=Received __1__ from __2__.
+receive-ap-catchup=Received __1__ as it is already checked.
+receive-sample-item=Received __1__x __2__
+sample-inventory-full=Additional items will be sent when inventory space is available.
+sample-error=Unable to receive __1__x [item=__2__] as this item does not exist.
+fail-to-place=Failed to place __1__ in __2__
+
+[traps]
+new-evolution-factor=New evolution factor: __1__
diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua
index 3f754d925ce9..3146e8042de2 100644
--- a/worlds/factorio/data/mod_template/control.lua
+++ b/worlds/factorio/data/mod_template/control.lua
@@ -420,12 +420,12 @@ function update_player(index)
sent = 0
end
if sent > 0 then
- player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]")
+ player.print({"archipelago.receive-sample-item", sent, "[item=" .. name .. ",quality="..stack.quality.."]"})
data.suppress_full_inventory_message = false
end
if sent ~= count then -- Couldn't full send.
if not data.suppress_full_inventory_message then
- player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
+ player.print({"archipelago.sample-inventory-full"}, {r=1, g=1, b=0.25})
end
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
samples[name] = count - sent -- Buffer the remaining items
@@ -434,7 +434,7 @@ function update_player(index)
samples[name] = nil -- Remove from the list
end
else
- player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
+ player.print({"archipelago.sample-inventory-full", count, name})
samples[name] = nil
end
end
@@ -665,7 +665,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
end
end
if new_entity == nil then
- force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius}))
+ force.print({"archipelago.fail-to-place", args.name, serpent.line({x = x, y = y, radius = radius})})
end
end
@@ -725,7 +725,7 @@ end,
local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") +
(TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis")))
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
- game.print({"", "New evolution factor:", new_factor})
+ game.print({"traps.new-evolution-factor", new_factor})
end,
["Teleport Trap"] = function()
for _, player in ipairs(game.forces["player"].players) do
@@ -777,10 +777,10 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if index == nil then
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
return
- elseif index == -1 then -- for coop sync and restoring from an older savegame
+ elseif index == "-1" then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech.researched ~= true then
- game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
+ game.print({"archipelago.receive-ap-catchup", "[technology=" .. tech.name .. "]"})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
@@ -792,7 +792,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
for _, item_name in ipairs(tech_stack) do
tech = force.technologies[item_name]
if tech.researched ~= true then
- game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
+ game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
return
@@ -804,7 +804,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if tech ~= nil then
storage.index_sync[index] = tech
if tech.researched ~= true then
- game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
+ game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
@@ -812,7 +812,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
elseif TRAP_TABLE[item_name] ~= nil then
if storage.index_sync[index] ~= item_name then -- not yet received trap
storage.index_sync[index] = item_name
- game.print({"", "Received ", item_name, " from ", source})
+ game.print({"archipelago.receive-ap-item", item_name, source})
TRAP_TABLE[item_name]()
end
else
diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua
index e45952301986..bfffab6a61a8 100644
--- a/worlds/factorio/data/mod_template/data-final-fixes.lua
+++ b/worlds/factorio/data/mod_template/data-final-fixes.lua
@@ -154,6 +154,13 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
+{%- if location.revealed %}
+new_tree_copy.localised_name = {"technology-name.ap-technology-full", "{{ player_names[item.player] }}", "{{ item.name }}", "{{ location.name }}"}
+new_tree_copy.localised_description = {"technology-description.ap-technology-full", "{{ item.name }}", "{{ player_names[item.player] }}", {% if item.advancement %}{"technology-description.ap-technology-item-advancement"}{% elif item.useful %}{"technology-description.ap-technology-item-useful"}{% elif item.trap %}{"technology-description.ap-technology-item-trap"}{% else %}""{% endif %}}
+{%- else %}
+new_tree_copy.localised_name = {"technology-name.ap-technology-hidden", "{{location.name}}"}
+new_tree_copy.localised_description = {"technology-description.ap-technology-hidden", {% if tech_tree_information == 1 and item.advancement %}{"technology-description.ap-technology-item-advancement"}{% else %}""{% endif %}}
+{% endif -%}
{% if location.crafted_item is not none %}
new_tree_copy.research_trigger = {
type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}",
diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua
index 43151ff00840..562999d5df66 100644
--- a/worlds/factorio/data/mod_template/data.lua
+++ b/worlds/factorio/data/mod_template/data.lua
@@ -13,7 +13,6 @@ end
local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"])
energy_bridge.name = "ap-energy-bridge"
energy_bridge.minable.result = "ap-energy-bridge"
-energy_bridge.localised_name = "Archipelago EnergyLink Bridge"
energy_bridge.energy_source.buffer_capacity = "50MJ"
energy_bridge.energy_source.input_flow_limit = "10MW"
energy_bridge.energy_source.output_flow_limit = "10MW"
@@ -25,7 +24,6 @@ data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge
local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"])
energy_bridge_item.name = "ap-energy-bridge"
-energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge"
energy_bridge_item.place_result = energy_bridge.name
tint_icon(energy_bridge_item, energy_bridge_tint())
data.raw["item"]["ap-energy-bridge"] = energy_bridge_item
@@ -35,7 +33,6 @@ energy_bridge_recipe.name = "ap-energy-bridge"
energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} }
energy_bridge_recipe.energy_required = 1
energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %}
-energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge"
data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen["basic"], "advanced_settings": world_gen["advanced"]}) }}
diff --git a/worlds/factorio/data/mod_template/locale/en/locale.cfg b/worlds/factorio/data/mod_template/locale/en/locale.cfg
deleted file mode 100644
index 59dcffcd6300..000000000000
--- a/worlds/factorio/data/mod_template/locale/en/locale.cfg
+++ /dev/null
@@ -1,31 +0,0 @@
-[map-gen-preset-name]
-archipelago=Archipelago
-
-[map-gen-preset-description]
-archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
-
-[technology-name]
-{% for location, item in locations %}
-{%- if location.revealed %}
-ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }})
-{%- else %}
-ap-{{ location.address }}-= {{location.name}}
-{%- endif -%}
-{% endfor %}
-
-[technology-description]
-{% for location, item in locations %}
-{%- if location.revealed %}
-ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}.
-{%- elif tech_tree_information == 1 and item.advancement %}
-ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement.
-{%- else %}
-ap-{{ location.address }}-=Researching this technology sends something to someone.
-{%- endif -%}
-{% endfor %}
-
-[mod-setting-name]
-archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
-
-[mod-setting-description]
-archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
\ No newline at end of file
diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua
index 41d30e58d552..c88c4325631c 100644
--- a/worlds/factorio/data/mod_template/settings.lua
+++ b/worlds/factorio/data/mod_template/settings.lua
@@ -21,6 +21,8 @@ data:extend({
type = "bool-setting",
name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}",
setting_type = "runtime-global",
+ localised_name = {"mod-setting-name.archipelago-death-link"},
+ localised_description = {"mod-setting-description.archipelago-death-link"},
{% if death_link %}
default_value = true
{% else %}
diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py
index 4cd80556ccf3..0d4a32c858e3 100644
--- a/worlds/generic/__init__.py
+++ b/worlds/generic/__init__.py
@@ -52,24 +52,3 @@ def create_item(self, name: str) -> Item:
if name == "Nothing":
return Item(name, ItemClassification.filler, -1, self.player)
raise InvalidItemError(name)
-
-@deprecated("worlds.generic.PlandoItem is deprecated and will be removed in the next version. "
- "Use Options.PlandoItem(s) instead.")
-class PlandoItem(NamedTuple):
- item: str
- location: str
- world: Union[bool, str] = False # False -> own world, True -> not own world
- from_pool: bool = True # if item should be removed from item pool
- force: str = 'silent' # false -> warns if item not successfully placed. true -> errors out on failure to place item.
-
- def warn(self, warning: str):
- if self.force in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
- logging.warning(f'{warning}')
- else:
- logging.debug(f'{warning}')
-
- def failed(self, warning: str, exception=Exception):
- if self.force in ['true', 'fail', 'failure']:
- raise exception(warning)
- else:
- self.warn(warning)
diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md
index 7dc0e6ba4c9a..6348ce2a5840 100644
--- a/worlds/generic/docs/advanced_settings_en.md
+++ b/worlds/generic/docs/advanced_settings_en.md
@@ -60,7 +60,7 @@ adding more randomness and "mystery" to your options. Every configurable setting
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
-are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
+are `description`, `name`, `game`, `quantity`, `requires`, and the name of the games you want options for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
using this to detail the intention of the file.
@@ -78,6 +78,9 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
* `game` is where either your chosen game goes or, if you would like, can be filled with multiple games each with
different weights.
+* `quantity` is the amount of times this yaml should be used when generating. This option is optional, the default value is 1.
+ To ensure that the name is unique with a value of at least two, the keywords from above must be used.
+
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version,
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md
index 625601d96b38..e8b4666fd028 100644
--- a/worlds/generic/docs/plando_en.md
+++ b/worlds/generic/docs/plando_en.md
@@ -99,7 +99,7 @@ case-sensitive. You can also use item groups and location groups that are define
## Item Plando Examples
```yaml
- plando_items:
+ plando_items:
# Example block - Pokémon Red and Blue
- items:
Potion: 3
diff --git a/worlds/hk/archipelago.json b/worlds/hk/archipelago.json
new file mode 100644
index 000000000000..d296027c1698
--- /dev/null
+++ b/worlds/hk/archipelago.json
@@ -0,0 +1 @@
+{"game": "Hollow Knight", "world_version": "0.0.0"}
diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py
index 9a2cb30293f0..f51545aa57cd 100644
--- a/worlds/jakanddaxter/__init__.py
+++ b/worlds/jakanddaxter/__init__.py
@@ -232,6 +232,9 @@ class JakAndDaxterWorld(World):
power_cell_thresholds_minus_one: list[int]
trap_weights: tuple[list[str], list[int]]
+ # UT Yaml-less flag
+ ut_can_gen_without_yaml = True
+
# Store these dictionaries for speed improvements.
level_to_regions: dict[str, list[JakAndDaxterRegion]] # Contains all levels and regions.
level_to_orb_regions: dict[str, list[JakAndDaxterRegion]] # Contains only regions which contain orbs.
@@ -243,6 +246,15 @@ def generate_early(self) -> None:
self.level_to_regions = defaultdict(list)
self.level_to_orb_regions = defaultdict(list)
+ # Implement Universal Tracker support - reset all options to those from UT's gen if applicable.
+ if hasattr(self.multiworld, "re_gen_passthrough"):
+ if jak1_name in self.multiworld.re_gen_passthrough:
+ for key, val in self.multiworld.re_gen_passthrough[jak1_name].items():
+ try:
+ getattr(self.options, key).value = val
+ except AttributeError:
+ pass
+
# Cache the power cell threshold values for quicker reference.
self.power_cell_thresholds = [
self.options.fire_canyon_cell_count.value,
diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py
index cb3d7c8d85ed..41457575d208 100644
--- a/worlds/kh2/Items.py
+++ b/worlds/kh2/Items.py
@@ -579,7 +579,6 @@
ItemName.IceCream,
ItemName.WaytotheDawn,
ItemName.IdentityDisk,
- ItemName.IceCream,
ItemName.NamineSketches
],
"AllVisitLocking": {
diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py
index 7e10dc29b7c1..c7ed1522566d 100644
--- a/worlds/kh2/__init__.py
+++ b/worlds/kh2/__init__.py
@@ -184,6 +184,8 @@ def create_items(self) -> None:
if self.visitlocking_dict[item] == 0:
self.visitlocking_dict.pop(item)
self.multiworld.push_precollected(self.create_item(item))
+ # tt is 3 visits so 2nd visit locking unlocks only the third visit
+ self.multiworld.push_precollected(self.create_item(ItemName.IceCream))
for _ in range(self.options.RandomVisitLockingItem.value):
if sum(self.visitlocking_dict.values()) <= 0:
diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md
index 55bdaa5bf175..66b648a4accc 100644
--- a/worlds/kh2/docs/setup_en.md
+++ b/worlds/kh2/docs/setup_en.md
@@ -14,7 +14,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.
2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago
- 1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
+ 1. [Archipelago Launcher](https://github.com/ArchipelagoMW/Archipelago/releases)
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
3. Install the mod from `TopazTK/KH2-ArchipelagoEnablers` using OpenKH Mod manager
1. Do Note that if you have `KH2FM-Mods-equations19/auto-save` OR `KH2FM-Mods-equations19/soft-reset` you should download `TopazTK/KH2-ArchipelagoEnablersLITE` instead
@@ -58,7 +58,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
## Using the KH2 Client
-Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
+Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoLauncher.exe](https://github.com/ArchipelagoMW/Archipelago/releases) and select the KH2 Client.
When you successfully connect to the server the client will automatically hook into the game to send/receive checks.
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.
diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py
index e36ff77a2484..d19ddab0b795 100644
--- a/worlds/ladx/__init__.py
+++ b/worlds/ladx/__init__.py
@@ -80,9 +80,10 @@ class DisplayMsgs(settings.Bool):
class GfxModFile(settings.FilePath):
"""
- Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
- Only .bin or .bdiff files
- The same directory will be checked for a matching text modification file
+ Gfxmod file, select one from the `Archipelago/data/sprites/ladx/` folder,
+ or make your own. You can generate a template here: https://ladx-gfx.3and3.dev/
+ Only .bin or .bdiff files.
+ Extended spritesheets from the upstream randomizer are not supported.
"""
def browse(self, filetypes=None, **kwargs):
filetypes = [("Binary / Patch files", [".bin", ".bdiff"])]
diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md
index a7f9f87ef9ad..ed4c0921ef83 100644
--- a/worlds/ladx/docs/setup_en.md
+++ b/worlds/ladx/docs/setup_en.md
@@ -12,8 +12,8 @@
1. Download and install [Archipelago](). **The installer
file is located in the assets section at the bottom of the version information.**
-2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
- This is your Links Awakening DX ROM file. This only needs to be done once..
+2. The first time you patch your game, you will be asked to locate your base ROM file.
+ This is your Links Awakening DX ROM file. This only needs to be done once.
3. You should assign your emulator as your default program for launching ROM
files.
diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py
index d5b104dde4b6..ae8f86262c77 100644
--- a/worlds/lufia2ac/__init__.py
+++ b/worlds/lufia2ac/__init__.py
@@ -48,7 +48,7 @@ class L2ACWorld(World):
"""
The Ancient Cave is a roguelike dungeon crawling game built into
the RGP Lufia II. Face 99 floors of ever harder to beat monsters,
- random items and find new companions on the way to face the Royal
+ random items and find new companions on the way to face the Master
Jelly in the end. Can you beat it?
"""
game: ClassVar[str] = "Lufia II Ancient Cave"
diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
index 4b5bf3f318fa..c78fae05fd48 100644
--- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
+++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
@@ -9,8 +9,8 @@ config file.
As you may or may not know, randomization was already a core feature of the Ancient Cave in Lufia II, basically being a
whole game within a game. The Ancient Cave has 99 floors with increasingly hard enemies, red chests and blue chests. At
-the end of the Ancient Cave you get to fight the Royal Jelly... if you make it that far. You cannot lose the Royal
-Jelly fight as it kills itself after giving you three rounds to try and kill it (or manage to vanquish your own party,
+the end of the Ancient Cave you get to fight the Master Jelly... if you make it that far. The Master
+Jelly gives you three rounds to try and kill it (or manage to vanquish your own party,
whichever one you can manage).
The Randomizer allows you to set different goals and modify the game in several other ways
diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md
index adb6e4ff616d..761c6a138ad6 100644
--- a/worlds/lufia2ac/docs/setup_en.md
+++ b/worlds/lufia2ac/docs/setup_en.md
@@ -56,7 +56,6 @@ If you would like to validate your config file to make sure it works, you may do
4. You will be presented with a server page, from which you can download your patch file.
5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and
open your emulator for you.
-6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
## Joining a MultiWorld Game
diff --git a/worlds/marioland2/__init__.py b/worlds/marioland2/__init__.py
index ea1354db6e65..82ce2ea5b0bf 100644
--- a/worlds/marioland2/__init__.py
+++ b/worlds/marioland2/__init__.py
@@ -56,6 +56,9 @@ class MarioLand2World(World):
web = MarioLand2WebWorld()
+ ut_can_gen_without_yaml = True
+ glitches_item_name = "ut_glitch"
+
item_name_groups = {
"Level Progression": {
item_name for item_name in items if item_name.endswith(("Progression", "Secret", "Secret 1", "Secret 2"))
@@ -94,42 +97,51 @@ def __init__(self, world, player: int):
self.max_coin_locations = {}
self.sprite_data = {}
self.coin_fragments_required = 0
+ self.ut = False
def generate_early(self):
- self.sprite_data = deepcopy(level_sprites)
- if self.options.randomize_enemies:
- randomize_enemies(self.sprite_data, self.random)
- if self.options.randomize_platforms:
- randomize_platforms(self.sprite_data, self.random)
+ if hasattr(self.multiworld, "re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough:
+ self.ut = True
+ for key, value in self.multiworld.re_gen_passthrough[self.game].items():
+ if hasattr(self.options, key):
+ getattr(self.options, key).value = value
+ else:
+ setattr(self, key, value)
+ else:
+ self.sprite_data = deepcopy(level_sprites)
- if self.options.marios_castle_midway_bell:
- self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
+ if self.options.marios_castle_midway_bell:
+ self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
+ if self.options.randomize_enemies:
+ randomize_enemies(self.sprite_data, self.random)
+ if self.options.randomize_platforms:
+ randomize_platforms(self.sprite_data, self.random)
- if self.options.auto_scroll_chances == "vanilla":
- self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
- else:
- self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
- for _ in range(32)]
-
- self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
- unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
- if not self.options.shuffle_midway_bells:
- unbeatable_scroll_levels.append("Pumpkin Zone 1")
- for level, i in enumerate(self.auto_scroll_levels):
- if i == 1:
- if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
- self.auto_scroll_levels[level] = 2
- elif self.options.auto_scroll_mode == "chaos":
- if (self.options.accessibility == "full"
- and level_id_to_name[level] in unbeatable_scroll_levels):
+ if self.options.auto_scroll_chances == "vanilla":
+ self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
+ else:
+ self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
+ for _ in range(32)]
+
+ self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
+ unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
+ if not self.options.shuffle_midway_bells:
+ unbeatable_scroll_levels.append("Pumpkin Zone 1")
+ for level, i in enumerate(self.auto_scroll_levels):
+ if i == 1:
+ if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
self.auto_scroll_levels[level] = 2
- else:
- self.auto_scroll_levels[level] = self.random.randint(1, 3)
- elif (self.options.accessibility == "full"
- and level_id_to_name[level] in unbeatable_scroll_levels):
- self.auto_scroll_levels[level] = 0
- if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
- self.auto_scroll_levels[level] = 3
+ elif self.options.auto_scroll_mode == "chaos":
+ if (self.options.accessibility == "full"
+ and level_id_to_name[level] in unbeatable_scroll_levels):
+ self.auto_scroll_levels[level] = 2
+ else:
+ self.auto_scroll_levels[level] = self.random.randint(1, 3)
+ elif (self.options.accessibility == "full"
+ and level_id_to_name[level] in unbeatable_scroll_levels):
+ self.auto_scroll_levels[level] = 0
+ if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
+ self.auto_scroll_levels[level] = 3
def create_regions(self):
menu_region = Region("Menu", self.player, self.multiworld)
@@ -178,7 +190,10 @@ def create_regions(self):
wario.place_locked_item(MarioLand2Item("Wario Defeated", ItemClassification.progression, None, self.player))
if self.options.coinsanity:
- coinsanity_checks = self.options.coinsanity_checks.value
+ if hasattr(self.multiworld, "generation_is_fake"):
+ coinsanity_checks = self.options.coinsanity_checks.range_end
+ else:
+ coinsanity_checks = self.options.coinsanity_checks.value
self.num_coin_locations = [[region, 1] for region in created_regions if region != "Mario's Castle"]
self.max_coin_locations = {region: len(coins_coords[region]) for region in created_regions
if region != "Mario's Castle"}
@@ -423,12 +438,40 @@ def create_items(self):
self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)]
def fill_slot_data(self):
- return {
- "energy_link": self.options.energy_link.value
- }
+ # Expose settings for accurate tracker logic
+ shark_count: int = [
+ self.multiworld.worlds[self.player].sprite_data["Turtle Zone 1"][i]["sprite"]
+ for i in (27, 28)
+ ].count("Shark")
+ crane_count: int = [
+ self.multiworld.worlds[self.player].sprite_data["Mario Zone 3"][i]["sprite"]
+ for i in (17, 18, 25)
+ ].count("Claw Grabber")
+ options_dict = self.options.as_dict(
+ "energy_link",
+ "shuffle_golden_coins",
+ "required_golden_coins",
+ "coinsanity",
+ "shuffle_midway_bells",
+ "marios_castle_midway_bell",
+ "shuffle_pipe_traversal",
+ "auto_scroll_mode"
+ )
+ options_dict.update({
+ "auto_scroll_levels": self.auto_scroll_levels,
+ "turtle_zone_1_shark_count": shark_count,
+ "mario_zone_3_crane_count": crane_count,
+ "coin_fragments_required": self.coin_fragments_required
+ })
+ return options_dict
+
+ @staticmethod
+ def interpret_slot_data(slot_data):
+ return slot_data
def create_item(self, name: str) -> Item:
- return MarioLand2Item(name, items[name], self.item_name_to_id[name], self.player)
+ return MarioLand2Item(name, items[name] if name in items else ItemClassification.progression,
+ self.item_name_to_id[name] if name in self.item_name_to_id else None, self.player)
def get_filler_item_name(self):
return "1 Coin"
diff --git a/worlds/marioland2/logic.py b/worlds/marioland2/logic.py
index 4ccb2eb4c919..ea4fc7cdd3c3 100644
--- a/worlds/marioland2/logic.py
+++ b/worlds/marioland2/logic.py
@@ -5,6 +5,8 @@ def is_auto_scroll(state, player, level):
level_id = level_name_to_id[level]
if state.has_any(["Cancel Auto Scroll", f"Cancel Auto Scroll - {level}"], player):
return False
+ if state.has("ut_glitch", player) and state.multiworld.worlds[player].auto_scroll_levels[level_id] == 3:
+ return state.has(f"Auto Scroll - {level}", player)
return state.multiworld.worlds[player].auto_scroll_levels[level_id] > 0
@@ -287,8 +289,12 @@ def mario_zone_3_coins(state, player, coins):
if state.has("Carrot", player):
reachable_spike_coins = 15
else:
- sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
- reachable_spike_coins = min(3, len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
+ if state.multiworld.worlds[player].ut:
+ claw_grabbers = state.multiworld.worlds[player].mario_zone_3_crane_count
+ else:
+ sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
+ claw_grabbers = len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
+ reachable_spike_coins = min(3, claw_grabbers
+ state.has("Mushroom", player) + state.has("Fire Flower", player)) * 5
reachable_coins += reachable_spike_coins
if not auto_scroll:
@@ -309,8 +315,11 @@ def mario_zone_4_coins(state, player, coins):
def not_blocked_by_sharks(state, player):
- sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
- for i in (27, 28)].count("Shark")
+ if state.multiworld.worlds[player].ut:
+ sharks = state.multiworld.worlds[player].turtle_zone_1_shark_count
+ else:
+ sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
+ for i in (27, 28)].count("Shark")
if state.has("Carrot", player) or not sharks:
return True
if sharks == 2:
diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py
index 84f7f9b24281..490803e9596f 100644
--- a/worlds/messenger/connections.py
+++ b/worlds/messenger/connections.py
@@ -316,7 +316,7 @@
"Searing Mega Shard Shop": [
"Searing Crags - Falling Rocks Shop",
"Searing Crags - Before Final Climb Shop",
- "Searing Crags - Key of Strength Shop",
+ "Searing Crags - Key of Strength Room",
],
"Before Final Climb Shop": [
"Searing Crags - Raining Rocks Checkpoint",
@@ -330,6 +330,9 @@
"Searing Crags - Top",
],
"Key of Strength Shop": [
+ "Searing Crags - Key of Strength Room",
+ ],
+ "Key of Strength Room": [
"Searing Crags - Searing Mega Shard Shop",
],
"Triple Ball Spinner Checkpoint": [
diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py
index 11fc6101087b..8e46eee17d6b 100644
--- a/worlds/messenger/portals.py
+++ b/worlds/messenger/portals.py
@@ -7,7 +7,6 @@
if TYPE_CHECKING:
from . import MessengerWorld
-
PORTALS: list[str] = [
"Autumn Hills",
"Riviere Turquoise",
@@ -17,7 +16,6 @@
"Glacial Peak",
]
-
SHOP_POINTS: dict[str, list[str]] = {
"Autumn Hills": [
"Climbing Claws",
@@ -112,7 +110,6 @@
]
}
-
CHECKPOINTS: dict[str, list[str]] = {
"Autumn Hills": [
"Hope Latch",
@@ -185,7 +182,6 @@
]
}
-
REGION_ORDER: list[str] = [
"Autumn Hills",
"Forlorn Temple",
@@ -306,4 +302,4 @@ def add_closed_portal_reqs(world: "MessengerWorld") -> None:
closed_portals = [entrance for entrance in PORTALS if f"{entrance} Portal" not in world.starting_portals]
for portal in closed_portals:
tower_exit = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player)
- tower_exit.access_rule = lambda state, portal_item=portal: state.has(portal_item, world.player)
+ tower_exit.access_rule = lambda state, portal_item=portal: state.has(f"{portal_item} Portal", world.player)
diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py
index d53b84fe3401..eb668356ca08 100644
--- a/worlds/messenger/regions.py
+++ b/worlds/messenger/regions.py
@@ -96,7 +96,7 @@
"Searing Crags - Power Thistle",
"Searing Crags - Astral Tea Leaves",
],
- "Searing Crags - Key of Strength Shop": [
+ "Searing Crags - Key of Strength Room": [
"Searing Crags - Key of Strength",
],
"Searing Crags - Portal": [
diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py
index bc1fc6aa9989..23f2bf32c0a2 100644
--- a/worlds/messenger/rules.py
+++ b/worlds/messenger/rules.py
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
-from BaseClasses import CollectionState, CollectionRule
+from BaseClasses import CollectionState, CollectionRule, Region
from worlds.generic.Rules import add_rule, allow_self_locking_items
from .constants import NOTES, PHOBEKINS
from .options import MessengerAccessibility
@@ -13,6 +13,7 @@ class MessengerRules:
player: int
world: "MessengerWorld"
connection_rules: dict[str, CollectionRule]
+ indirect_conditions: dict[str, list[Region]]
region_rules: dict[str, CollectionRule]
location_rules: dict[str, CollectionRule]
maximum_price: int
@@ -108,17 +109,18 @@ def __init__(self, world: "MessengerWorld") -> None:
"Searing Crags - Right -> Searing Crags - Portal":
lambda state: self.has_tabi(state) and self.has_wingsuit(state),
"Searing Crags - Colossuses Shop -> Searing Crags - Key of Strength Shop":
- lambda state: state.has("Power Thistle", self.player)
- and (self.has_dart(state)
- or (self.has_wingsuit(state)
- and self.can_destroy_projectiles(state))),
+ lambda state: state.has("Power Thistle", self.player),
+ "Searing Crags - Key of Strength Shop -> Searing Crags - Key of Strength Room":
+ lambda state: self.has_dart(state)
+ or (self.has_wingsuit(state)
+ and self.can_destroy_projectiles(state)),
"Searing Crags - Falling Rocks Shop -> Searing Crags - Searing Mega Shard Shop":
self.has_dart,
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Before Final Climb Shop":
lambda state: self.has_dart(state) or self.can_destroy_projectiles(state),
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Falling Rocks Shop":
self.has_dart,
- "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room":
self.false,
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
self.has_dart,
@@ -219,6 +221,16 @@ def __init__(self, world: "MessengerWorld") -> None:
lambda state: self.can_dboost(state) or self.has_dart(state),
}
+ # dict of connection names and the regions checked in the requirements to traverse the exit
+ self.indirect_conditions = {
+ "Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Crushing Pits Shop": [
+ self.world.get_region("Howling Grotto - Emerald Golem Shop")
+ ],
+ "Glacial Peak - Left -> Elemental Skylands - Air Shmup": [
+ self.world.get_location("Quillshroom Marsh - Queen of Quills").parent_region
+ ],
+ }
+
self.location_rules = {
# hq
"Money Wrench": self.can_shop,
@@ -364,6 +376,8 @@ def set_messenger_rules(self) -> None:
for entrance_name, rule in self.connection_rules.items():
entrance = multiworld.get_entrance(entrance_name, self.player)
entrance.access_rule = rule
+ for region in self.indirect_conditions.get(entrance_name, ()):
+ multiworld.register_indirect_condition(region, entrance)
for loc in multiworld.get_locations(self.player):
if loc.name in self.location_rules:
loc.access_rule = self.location_rules[loc.name]
@@ -406,7 +420,7 @@ def __init__(self, world: "MessengerWorld") -> None:
lambda state: self.has_dart(state) or
(self.can_destroy_projectiles(state) and
(self.has_wingsuit(state) or self.can_dboost(state))),
- "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room":
lambda state: self.can_leash(state) or self.has_windmill(state),
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
self.true,
@@ -512,7 +526,6 @@ def __init__(self, world: "MessengerWorld") -> None:
self.location_rules = {
"Bamboo Creek - Claustro": self.has_wingsuit,
- "Searing Crags - Key of Strength": self.has_wingsuit,
"Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
"Searing Crags - Pyro": self.has_tabi,
"Underworld - Key of Chaos": self.has_tabi,
diff --git a/worlds/messenger/test/test_portals.py b/worlds/messenger/test/test_portals.py
index b1875ac0b3ab..416b1f6a6af8 100644
--- a/worlds/messenger/test/test_portals.py
+++ b/worlds/messenger/test/test_portals.py
@@ -35,3 +35,29 @@ def test_portal_reqs(self) -> None:
test_state.collect(item)
self.assertTrue(entrance.can_reach(test_state), grouping)
entrance.access_rule = lambda state: True
+
+
+class PortalUnlockTest(MessengerTestBase):
+ options = {
+ "available_portals": 3,
+ }
+
+ def test_unlocking_portal(self) -> None:
+ """Validate that unlocking the portal event actually unlock the portal in HQ"""
+
+ print(self.world.starting_portals)
+
+ for portal in PORTALS:
+ name = f"{portal} Portal"
+ if name in self.world.starting_portals:
+ continue
+
+ entrance_name = f"ToTHQ {name}"
+ with self.subTest(portal=name, entrance_name=entrance_name):
+ hq_portal = self.multiworld.get_entrance(entrance_name, self.player)
+ test_state = CollectionState(self.multiworld)
+ self.assertFalse(hq_portal.can_reach(test_state), "reachable with nothing")
+
+ event = self.multiworld.get_location(name, self.player)
+ test_state.collect(event.item)
+ self.assertTrue(hq_portal.can_reach(test_state))
diff --git a/worlds/mmbn3/Rom.py b/worlds/mmbn3/Rom.py
index 347375c50356..7cb26455a930 100644
--- a/worlds/mmbn3/Rom.py
+++ b/worlds/mmbn3/Rom.py
@@ -341,7 +341,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if CHECKSUM_BLUE != basemd5.hexdigest():
- raise Exception('Supplied Base Rom does not match US GBA Blue Version.'
+ raise Exception('Supplied Base Rom does not match US GBA Blue Version. '
'Please provide the correct ROM version')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py
index 6cb294bd98a6..6b7bbbfe7b6c 100644
--- a/worlds/musedash/MuseDashData.py
+++ b/worlds/musedash/MuseDashData.py
@@ -712,4 +712,18 @@
"Chasing Daylight": SongData(2900836, "96-0", "Wuthering Waves Pioneer Podcast", False, 3, 5, 8),
"CATCH ME IF YOU CAN": SongData(2900837, "96-1", "Wuthering Waves Pioneer Podcast", False, 4, 6, 9),
"RUNNING FOR YOUR LIFE": SongData(2900838, "96-2", "Wuthering Waves Pioneer Podcast", False, 2, 5, 8),
+ "Denpa-tic Imaginary Girl": SongData(2900839, "43-73", "MD Plus Project", False, 4, 6, 9),
+ "Don't Fight The Music": SongData(2900840, "97-0", "DASH AND SHOOT!", False, 6, 8, 10),
+ "Viyella's Scream": SongData(2900841, "97-1", "DASH AND SHOOT!", False, 7, 9, 11),
+ "Final Flash Flight": SongData(2900842, "97-2", "DASH AND SHOOT!", False, 5, 7, 10),
+ "YURUSHITE": SongData(2900843, "97-3", "DASH AND SHOOT!", False, 6, 8, 11),
+ "girls.exe": SongData(2900844, "97-4", "DASH AND SHOOT!", False, 6, 8, 11),
+ "Baqeela": SongData(2900845, "97-5", "DASH AND SHOOT!", False, 6, 8, 10),
+ "Help me, ERINNNNNN!!": SongData(2900846, "43-74", "MD Plus Project", False, 4, 8, 10),
+ "Utakata, Ai no Mahoroba": SongData(2900847, "98-0", "Touhou Mugakudan -V-", False, 3, 5, 7),
+ "dynamite": SongData(2900848, "98-1", "Touhou Mugakudan -V-", False, 5, 7, 9),
+ "Matsuyoi Nightbug": SongData(2900849, "98-2", "Touhou Mugakudan -V-", False, 5, 7, 10),
+ "Coooonsultant!": SongData(2900850, "98-3", "Touhou Mugakudan -V-", False, 6, 8, 10),
+ "Stop at the affected part and melt quickly - Madness Udine Quarter": SongData(2900851, "98-4", "Touhou Mugakudan -V-", False, 4, 6, 10),
+ "Ultimate taste": SongData(2900852, "98-5", "Touhou Mugakudan -V-", False, 6, 8, 11),
}
\ No newline at end of file
diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py
index 55767cf04b65..97dfc12b08f2 100644
--- a/worlds/musedash/__init__.py
+++ b/worlds/musedash/__init__.py
@@ -3,6 +3,7 @@
from typing import List, ClassVar, Type, Set
from math import floor
from Options import PerGameCommonOptions, OptionError
+from rule_builder.rules import Has
from .Options import MuseDashOptions, md_option_groups
from .Items import MuseDashSongItem, MuseDashFixedItem
@@ -33,7 +34,16 @@ class MuseDashWebWorld(WebWorld):
["Shiny"]
)
- tutorials = [setup_en, setup_es]
+ setup_it = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Italiano",
+ "setup_it.md",
+ "setup/it",
+ ["UCSA"]
+ )
+
+ tutorials = [setup_en, setup_es, setup_it]
options_presets = MuseDashPresets
option_groups = md_option_groups
@@ -284,17 +294,17 @@ def create_regions(self) -> None:
# Adds 2 item locations per song/album to the menu region.
for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i]
+ rule = Has(name)
loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region)
- loc1.access_rule = lambda state, place=name: state.has(place, self.player)
+ self.set_rule(loc1, rule)
menu_region.locations.append(loc1)
loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region)
- loc2.access_rule = lambda state, place=name: state.has(place, self.player)
+ self.set_rule(loc2, rule)
menu_region.locations.append(loc2)
def set_rules(self) -> None:
- self.multiworld.completion_condition[self.player] = lambda state: \
- state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
+ self.set_completion_rule(Has(self.md_collection.MUSIC_SHEET_NAME, self.get_music_sheet_win_count()))
def get_available_traps(self) -> List[str]:
full_trap_list = self.md_collection.trap_items.keys()
diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json
index 0580d85e77c1..4f32398442ec 100644
--- a/worlds/musedash/archipelago.json
+++ b/worlds/musedash/archipelago.json
@@ -1,6 +1,6 @@
{
"game": "Muse Dash",
"authors": ["DeamonHunter"],
- "world_version": "1.5.30",
+ "world_version": "1.5.33",
"minimum_ap_version": "0.6.3"
}
\ No newline at end of file
diff --git a/worlds/musedash/docs/setup_it.md b/worlds/musedash/docs/setup_it.md
new file mode 100644
index 000000000000..22ace7bb51bf
--- /dev/null
+++ b/worlds/musedash/docs/setup_it.md
@@ -0,0 +1,55 @@
+# Guida al Setup di Muse Dash per Archipelago
+
+## Links
+- [Pagina Principale](../../../../games/Muse%20Dash/info/en)
+- [Opzioni](../../../../games/Muse%20Dash/player-options)
+
+## Software Richiesto
+
+- Windows 8 o più recente.
+- Muse Dash: [Disponibile su Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
+ - \[Facoltativo\] DLC [Muse Plus]: [Disponibile su Steam](https://store.steampowered.com/app/2593750/Muse_Dash_Muse_Plus/)
+- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
+ - L'installer potrebbe richiedere .Net Framework 4.8: [Download](https://dotnet.microsoft.com/it-it/download/dotnet-framework/net48)
+- .NET Desktop Runtime 6.0.XX (Se non installato in precedenza): [Download](https://dotnet.microsoft.com/it-it/download/dotnet/6.0)
+- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
+
+## Installare la mod di Archipelago per Muse Dash
+
+1. Scarica [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) ed eseguilo.
+2. Seleziona la scheda "automated", premi select e naviga fino a `MuseDash.exe`.
+ - Puoi trovare la cartella tramite steam premendo tasto destro sul gioco nella tua libreria e scegliendo *Gestisci→Sfoglia i file locali*
+ - Se clicki sulla barra nella parte superiore della finestra, che ti dice la cartella attuale, questa ti darà un percorso che potrai copiare.
+ Se copi questo percorso nella finestra creata da **MelonLoader** il programma navigherà automaticamente fino a quella cartella.
+3. Seleziona v0.7.0. e premi "install".
+4. Esegui il gioco una volta e aspetta fino alla comparsa del menù iniziale di Muse Dash prima di chiuderlo.
+5. Scarica l'ultima versione della [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
+ ed estraila nella cartella `/Mods/` appena creata nella cartella di installazione di Muse Dash.
+ - Tutti i file devono essere nella cartella `/Mods/` e non in una cartella al suo interno.
+
+Se hai installato tutto correttamente, dovrebbe apparire un bottone in basso a destra che ti permetterà di effettuare il login ad un server di Archipelago.
+
+## Generare una Sessione MultiWorld
+1. Visita la pagina [Player Options](/games/Muse%20Dash/player-options) e configura a tuo piacimento le opzioni specifiche per il gioco.
+2. Esporta il tuo file yaml e usalo per generare una nuova sessione randomizzata
+ - (Per istruizioni su come generare una nuova sessione di Archipelago, fai riferimento alla [Archipelago Web Guide](/tutorial/Archipelago/setup/en))
+
+## Entrare in una Sessione MultiWorld
+
+1. Esegui Muse Dash e supera la schermata iniziale. Premi il bottone in basso a destra.
+2. Inserisci i dettagli della sessione di Archipelago, come l'indirizzo del server con la sua porta (per esempio, archipelago.gg:38381), nome utente e password.
+3. Se tutto è stato inserito correttamente, la finestra dovrebbe scomparire e dovrebbe apparire il menù principale.
+ Una volta entrato nella schermata di selezione canzoni dovrebbe esserne disponibile un numero ridotto.
+
+## Risoluzione Problemi
+
+### No Support Module Loaded
+
+Questo errore avviene quando Melon Loader non è in grado di trovare i file necessari per eseguire le mod. Generalmente ci sono due cause principali per questo errore:
+un errore nella generazione dei file quando il gioco è stato avviato con Melon Loader per la prima volta o la rimozione di file dopo la generazione da parte di un antivirus.
+
+Per risolvere questo problema devi per prima cosa rimuovere Melon Loader da Muse Dash.
+Puoi fare ciò eliminando la cartella di Melon Loader all'interno della cartella di Muse Dash, dopodichè puoi seguire nuovamente i passaggi per l'installazione.
+
+Se continui ad avere lo stesso problema e stai usando un antivirus, prova a disattivarlo temporaneamente quando esegui Muse Dash per la prima volta
+o aggiungi la cartella di Muse Dash alla whitelist.
diff --git a/worlds/musedash/test/TestLocationRules.py b/worlds/musedash/test/TestLocationRules.py
new file mode 100644
index 000000000000..103130b8cf73
--- /dev/null
+++ b/worlds/musedash/test/TestLocationRules.py
@@ -0,0 +1,69 @@
+from . import MuseDashTestBase
+from typing import List
+
+
+class LocationRules(MuseDashTestBase):
+ CHECK_SONGS: List[str] = [
+ "Magical Wonderland",
+ "Iyaiya",
+ "Wonderful Pain",
+ "Breaking Dawn",
+ "One-Way Subway",
+ "Frost Land",
+ "Heart-Pounding Flight",
+ "Pancake is Love",
+ "Shiguang Tuya",
+ "Evolution",
+ "Dolphin and Broadcast",
+ "Yuki no Shizuku Ame no Oto",
+ "Best One feat.tooko",
+ "Candy-coloured Love Theory",
+ "Night Wander",
+ "Dohna Dohna no Uta",
+ "Spring Carnival",
+ "DISCO NIGHT",
+ "Koi no Moonlight"
+ ]
+
+ options = {
+ "starting_song_count": 3,
+ "additional_song_count": 15,
+ "streamer_mode_enabled": True,
+ "include_songs": CHECK_SONGS
+ }
+
+
+ def test_rules(self):
+ """Due to me typoing the second rule of a location, this test exists to ensure that doesn't happen again"""
+ muse_dash_world = self.get_world()
+
+ for song in self.CHECK_SONGS:
+ if song == muse_dash_world.victory_song_name:
+ continue
+
+ if song in muse_dash_world.starting_songs:
+ self.assertTrue(self.can_reach_location(song + "-0"), f"Starting Location {song}-0 was not beatable.")
+ self.assertTrue(self.can_reach_location(song + "-1"), f"Starting Location {song}-1 was not beatable.")
+ continue
+
+
+ self.assertFalse(self.can_reach_location(song + "-0"), f"Location {song}-0 was unlocked without an item.")
+ self.assertFalse(self.can_reach_location(song + "-1"), f"Location {song}-1 was unlocked without an item.")
+ self.collect_by_name(song)
+ self.assertTrue(self.can_reach_location(song + "-0"), f"Location {song}-0 was not unlocked with its item.")
+ self.assertTrue(self.can_reach_location(song + "-1"), f"Location {song}-1 was not unlocked with its item.")
+
+
+ sheets = self.get_items_by_name("Music Sheet")
+ sheets_to_win = muse_dash_world.get_music_sheet_win_count()
+
+ for sheet in sheets:
+ if sheets_to_win <= 0:
+ break
+
+ self.assertBeatable(False)
+ self.collect(sheet)
+ sheets_to_win -= 1
+
+ self.assertBeatable(True)
+
\ No newline at end of file
diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py
index fd8d0ebc7d58..8150d2b2448f 100644
--- a/worlds/pokemon_emerald/locations.py
+++ b/worlds/pokemon_emerald/locations.py
@@ -177,7 +177,7 @@ def set_legendary_cave_entrances(world: "PokemonEmeraldWorld") -> None:
terra_cave_location_location = world.multiworld.get_location("TERRA_CAVE_LOCATION", world.player)
terra_cave_location_location.item = None
terra_cave_location_location.place_locked_item(world.create_event(terra_cave_location_name))
-
+
marine_cave_location_name = world.random.choice([
"MARINE_CAVE_ROUTE_105_1",
"MARINE_CAVE_ROUTE_105_2",
diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py
index 9529be877ebe..ebd36898ab25 100644
--- a/worlds/pokemon_emerald/options.py
+++ b/worlds/pokemon_emerald/options.py
@@ -594,7 +594,7 @@ class NormalizeEncounterRates(Toggle):
Make every slot on an encounter table approximately equally likely.
This does NOT mean each species is equally likely. In the vanilla game, each species may occupy more than one slot, and slots vary in probability.
-
+
Species will still occupy the same slots as vanilla, but the slots will be equally weighted. The minimum encounter rate will be 8% (higher in water).
"""
display_name = "Normalize Encounter Rates"
diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py
index 76285d11dab8..d51b80f35302 100644
--- a/worlds/pokemon_emerald/pokemon.py
+++ b/worlds/pokemon_emerald/pokemon.py
@@ -502,7 +502,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
species.learnset = new_learnset
-
+
def randomize_starters(world: "PokemonEmeraldWorld") -> None:
if world.options.starters == RandomizeStarters.option_vanilla:
return
@@ -682,7 +682,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
]
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
-
+
player_filtered_candidates = [
species
for species in candidates
diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py
index 30ebf72e4d63..ce3d48ca1542 100644
--- a/worlds/pokemon_emerald/rules.py
+++ b/worlds/pokemon_emerald/rules.py
@@ -1,14 +1,16 @@
"""
Logic rule definitions for Pokemon Emerald
"""
-from typing import TYPE_CHECKING, Callable, Dict
+from collections import defaultdict
+from typing import TYPE_CHECKING, Literal
-from BaseClasses import CollectionState
-from worlds.generic.Rules import add_rule, set_rule
+from rule_builder.rules import (Rule, CanReachEntrance, Has, HasAll, HasAny, HasFromListUnique, HasGroupUnique,
+ OptionFilter, True_)
from .data import LocationCategory, NATIONAL_ID_TO_SPECIES_ID, NUM_REAL_SPECIES, data
from .locations import PokemonEmeraldLocation
-from .options import DarkCavesRequireFlash, EliteFourRequirement, NormanRequirement, Goal
+from .options import (DarkCavesRequireFlash, EliteFourRequirement, NormanRequirement, Goal, ModifyRoute118,
+ ExtraBoulders, ExtraBumpySlope, RemoveRoadblocks)
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
@@ -17,23 +19,18 @@
# Rules are organized by town/route/dungeon and ordered approximately
# by when you would first reach that place in a vanilla playthrough.
def set_rules(world: "PokemonEmeraldWorld") -> None:
- hm_rules: Dict[str, Callable[[CollectionState], bool]] = {}
+ entrance_rules: defaultdict[str, Rule] = defaultdict(True_)
+ location_rules: defaultdict[str, Rule] = defaultdict(True_)
+
+ hm_rules: dict[str, Rule] = {}
for hm, badges in world.hm_requirements.items():
if isinstance(badges, list):
- hm_rules[hm] = lambda state, hm=hm, badges=badges: \
- state.has(hm, world.player) and state.has_all(badges, world.player)
+ hm_rules[hm] = Has(hm) & HasAll(*badges)
else:
- hm_rules[hm] = lambda state, hm=hm, badges=badges: \
- state.has(hm, world.player) and state.has_group_unique("Badge", world.player, badges)
-
- def has_acro_bike(state: CollectionState):
- return state.has("Acro Bike", world.player)
-
- def has_mach_bike(state: CollectionState):
- return state.has("Mach Bike", world.player)
+ hm_rules[hm] = Has(hm) & HasGroupUnique("Badge", badges)
- def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool:
- return state.has_from_list_unique([
+ def create_defeated_n_gym_leaders_rule(n: int) -> Rule:
+ return HasFromListUnique(
"EVENT_DEFEAT_ROXANNE",
"EVENT_DEFEAT_BRAWLY",
"EVENT_DEFEAT_WATTSON",
@@ -42,7 +39,8 @@ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool:
"EVENT_DEFEAT_WINONA",
"EVENT_DEFEAT_TATE_AND_LIZA",
"EVENT_DEFEAT_JUAN",
- ], world.player, n)
+ count=n
+ )
huntable_legendary_events = [
f"EVENT_ENCOUNTER_{key}"
@@ -63,1485 +61,573 @@ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool:
if name in world.options.allowed_legendary_hunt_encounters.value
]
- def encountered_n_legendaries(state: CollectionState, n: int) -> bool:
- return state.has_from_list_unique(huntable_legendary_events, world.player, n)
-
- def get_entrance(entrance: str):
- return world.multiworld.get_entrance(entrance, world.player)
-
- def get_location(location: str):
- if location in data.locations:
- location = data.locations[location].label
-
- return world.multiworld.get_location(location, world.player)
-
- if world.options.goal == Goal.option_champion:
- completion_condition = lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- elif world.options.goal == Goal.option_steven:
- completion_condition = lambda state: state.has("EVENT_DEFEAT_STEVEN", world.player)
- elif world.options.goal == Goal.option_norman:
- completion_condition = lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
- elif world.options.goal == Goal.option_legendary_hunt:
- completion_condition = lambda state: encountered_n_legendaries(state, world.options.legendary_hunt_count.value)
-
- world.multiworld.completion_condition[world.player] = completion_condition
+ world.set_completion_rule(
+ (OptionFilter(Goal, Goal.option_champion) & Has("EVENT_DEFEAT_CHAMPION")) |
+ (OptionFilter(Goal, Goal.option_steven) & Has("EVENT_DEFEAT_STEVEN")) |
+ (OptionFilter(Goal, Goal.option_norman) & Has("EVENT_DEFEAT_NORMAN")) |
+ (OptionFilter(Goal, Goal.option_legendary_hunt) & HasFromListUnique(
+ *huntable_legendary_events,
+ count=world.options.legendary_hunt_count.value,
+ ))
+ )
if world.options.legendary_hunt_catch:
- set_rule(get_location("EVENT_ENCOUNTER_GROUDON"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_KYOGRE"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_RAYQUAZA"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_LATIAS"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ location_rules["EVENT_ENCOUNTER_GROUDON"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_KYOGRE"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_RAYQUAZA"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_LATIAS"] = Has("EVENT_DEFEAT_CHAMPION")
# Latios already only requires defeating the champion and access to Route 117
- # set_rule(get_location("EVENT_ENCOUNTER_LATIOS"),
- # lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_REGIROCK"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_REGICE"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_REGISTEEL"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_MEW"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_DEOXYS"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_HO_OH"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
- set_rule(get_location("EVENT_ENCOUNTER_LUGIA"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ # location_rules["EVENT_ENCOUNTER_LATIOS"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_REGIROCK"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_REGICE"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_REGISTEEL"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_MEW"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_DEOXYS"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_HO_OH"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["EVENT_ENCOUNTER_LUGIA"] = Has("EVENT_DEFEAT_CHAMPION")
# Sky
- set_rule(
- get_entrance("REGION_LITTLEROOT_TOWN/MAIN -> REGION_SKY"),
- hm_rules["HM02 Fly"]
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_LITTLEROOT_TOWN/MAIN"),
- lambda state: state.has("EVENT_VISITED_LITTLEROOT_TOWN", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_OLDALE_TOWN/MAIN"),
- lambda state: state.has("EVENT_VISITED_OLDALE_TOWN", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_PETALBURG_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_PETALBURG_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_RUSTBORO_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_RUSTBORO_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_DEWFORD_TOWN/MAIN"),
- lambda state: state.has("EVENT_VISITED_DEWFORD_TOWN", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_SLATEPORT_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_SLATEPORT_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_MAUVILLE_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_MAUVILLE_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_VERDANTURF_TOWN/MAIN"),
- lambda state: state.has("EVENT_VISITED_VERDANTURF_TOWN", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_FALLARBOR_TOWN/MAIN"),
- lambda state: state.has("EVENT_VISITED_FALLARBOR_TOWN", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_LAVARIDGE_TOWN/MAIN"),
- lambda state: state.has("EVENT_VISITED_LAVARIDGE_TOWN", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_FORTREE_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_FORTREE_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_LILYCOVE_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_LILYCOVE_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_MOSSDEEP_CITY/MAIN"),
- lambda state: state.has("EVENT_VISITED_MOSSDEEP_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_SOOTOPOLIS_CITY/EAST"),
- lambda state: state.has("EVENT_VISITED_SOOTOPOLIS_CITY", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY -> REGION_EVER_GRANDE_CITY/SOUTH"),
- lambda state: state.has("EVENT_VISITED_EVER_GRANDE_CITY", world.player)
- )
+ entrance_rules["REGION_LITTLEROOT_TOWN/MAIN -> REGION_SKY"] = hm_rules["HM02 Fly"]
+ entrance_rules["REGION_SKY -> REGION_LITTLEROOT_TOWN/MAIN"] = Has("EVENT_VISITED_LITTLEROOT_TOWN")
+ entrance_rules["REGION_SKY -> REGION_OLDALE_TOWN/MAIN"] = Has("EVENT_VISITED_OLDALE_TOWN")
+ entrance_rules["REGION_SKY -> REGION_PETALBURG_CITY/MAIN"] = Has("EVENT_VISITED_PETALBURG_CITY")
+ entrance_rules["REGION_SKY -> REGION_RUSTBORO_CITY/MAIN"] = Has("EVENT_VISITED_RUSTBORO_CITY")
+ entrance_rules["REGION_SKY -> REGION_DEWFORD_TOWN/MAIN"] = Has("EVENT_VISITED_DEWFORD_TOWN")
+ entrance_rules["REGION_SKY -> REGION_SLATEPORT_CITY/MAIN"] = Has("EVENT_VISITED_SLATEPORT_CITY")
+ entrance_rules["REGION_SKY -> REGION_MAUVILLE_CITY/MAIN"] = Has("EVENT_VISITED_MAUVILLE_CITY")
+ entrance_rules["REGION_SKY -> REGION_VERDANTURF_TOWN/MAIN"] = Has("EVENT_VISITED_VERDANTURF_TOWN")
+ entrance_rules["REGION_SKY -> REGION_FALLARBOR_TOWN/MAIN"] = Has("EVENT_VISITED_FALLARBOR_TOWN")
+ entrance_rules["REGION_SKY -> REGION_LAVARIDGE_TOWN/MAIN"] = Has("EVENT_VISITED_LAVARIDGE_TOWN")
+ entrance_rules["REGION_SKY -> REGION_FORTREE_CITY/MAIN"] = Has("EVENT_VISITED_FORTREE_CITY")
+ entrance_rules["REGION_SKY -> REGION_LILYCOVE_CITY/MAIN"] = Has("EVENT_VISITED_LILYCOVE_CITY")
+ entrance_rules["REGION_SKY -> REGION_MOSSDEEP_CITY/MAIN"] = Has("EVENT_VISITED_MOSSDEEP_CITY")
+ entrance_rules["REGION_SKY -> REGION_SOOTOPOLIS_CITY/EAST"] = Has("EVENT_VISITED_SOOTOPOLIS_CITY")
+ entrance_rules["REGION_SKY -> REGION_EVER_GRANDE_CITY/SOUTH"] = Has("EVENT_VISITED_EVER_GRANDE_CITY")
# Littleroot Town
- set_rule(
- get_location("NPC_GIFT_RECEIVED_SS_TICKET"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_AURORA_TICKET"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_EON_TICKET"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_MYSTIC_TICKET"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_OLD_SEA_MAP"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_SS_TICKET"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["NPC_GIFT_RECEIVED_AURORA_TICKET"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["NPC_GIFT_RECEIVED_EON_TICKET"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["NPC_GIFT_RECEIVED_MYSTIC_TICKET"] = Has("EVENT_DEFEAT_CHAMPION")
+ location_rules["NPC_GIFT_RECEIVED_OLD_SEA_MAP"] = Has("EVENT_DEFEAT_CHAMPION")
# Route 102
- set_rule(
- get_entrance("REGION_ROUTE102/MAIN -> REGION_ROUTE102/POND"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_ROUTE102/MAIN -> REGION_ROUTE102/POND"] = hm_rules["HM03 Surf"]
# Route 103
- set_rule(
- get_entrance("REGION_ROUTE103/EAST -> REGION_ROUTE103/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE103/WEST -> REGION_ROUTE103/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE103/EAST -> REGION_ROUTE103/EAST_TREE_MAZE"),
- hm_rules["HM01 Cut"]
- )
+ entrance_rules["REGION_ROUTE103/EAST -> REGION_ROUTE103/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE103/WEST -> REGION_ROUTE103/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE103/EAST -> REGION_ROUTE103/EAST_TREE_MAZE"] = hm_rules["HM01 Cut"]
# Petalburg City
- set_rule(
- get_entrance("REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/SOUTH_POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/NORTH_POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_HM_SURF"),
- lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
- )
- if world.options.norman_requirement == NormanRequirement.option_badges:
- set_rule(
- get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
- lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value)
- )
- set_rule(
- get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
- lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value)
- )
- else:
- set_rule(
- get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
- lambda state: defeated_n_gym_leaders(state, world.options.norman_count.value)
+ location_rules["NPC_GIFT_RECEIVED_HM_SURF"] = Has("EVENT_DEFEAT_NORMAN")
+
+ entrance_rules["REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/SOUTH_POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/NORTH_POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"] = (
+ (
+ OptionFilter(NormanRequirement, NormanRequirement.option_badges) &
+ HasGroupUnique("Badge", world.options.norman_count.value)
+ ) |
+ (
+ OptionFilter(NormanRequirement, NormanRequirement.option_gyms) &
+ create_defeated_n_gym_leaders_rule(world.options.norman_count.value)
)
- set_rule(
- get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
- lambda state: defeated_n_gym_leaders(state, world.options.norman_count.value)
+ )
+ entrance_rules["MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"] = (
+ (
+ OptionFilter(NormanRequirement, NormanRequirement.option_badges) &
+ HasGroupUnique("Badge", world.options.norman_count.value)
+ ) |
+ (
+ OptionFilter(NormanRequirement, NormanRequirement.option_gyms) &
+ create_defeated_n_gym_leaders_rule(world.options.norman_count.value)
)
+ )
# Route 104
- set_rule(
- get_entrance("REGION_ROUTE104/SOUTH -> REGION_ROUTE104/SOUTH_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE104/NORTH -> REGION_ROUTE104/NORTH_POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE104/NORTH -> REGION_ROUTE104/TREE_ALCOVE_2"),
- hm_rules["HM01 Cut"]
- )
- set_rule(
- get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN"),
- lambda state: state.has("EVENT_TALK_TO_MR_STONE", world.player)
- )
+ entrance_rules["REGION_ROUTE104/SOUTH -> REGION_ROUTE104/SOUTH_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE104/NORTH -> REGION_ROUTE104/NORTH_POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE104/NORTH -> REGION_ROUTE104/TREE_ALCOVE_2"] = hm_rules["HM01 Cut"]
+ entrance_rules["REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN"] = Has("EVENT_TALK_TO_MR_STONE")
# Petalburg Woods
- set_rule(
- get_entrance("REGION_PETALBURG_WOODS/WEST_PATH -> REGION_PETALBURG_WOODS/EAST_PATH"),
- hm_rules["HM01 Cut"]
- )
+ entrance_rules["REGION_PETALBURG_WOODS/WEST_PATH -> REGION_PETALBURG_WOODS/EAST_PATH"] = hm_rules["HM01 Cut"]
# Rustboro City
- set_rule(
- get_location("EVENT_RETURN_DEVON_GOODS"),
- lambda state: state.has("EVENT_RECOVER_DEVON_GOODS", world.player)
- )
+ location_rules["EVENT_RETURN_DEVON_GOODS"] = Has("EVENT_RECOVER_DEVON_GOODS")
if world.options.trainersanity:
- set_rule(
- get_location("TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD"),
- lambda state: state.has("EVENT_RETURN_DEVON_GOODS", world.player)
- )
+ location_rules["TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD"] = Has("EVENT_RETURN_DEVON_GOODS")
# Devon Corp
- set_rule(
- get_entrance("MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0"),
- lambda state: state.has("EVENT_RETURN_DEVON_GOODS", world.player)
- )
+ entrance_rules["MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0"] = Has("EVENT_RETURN_DEVON_GOODS")
# Route 116
- set_rule(
- get_entrance("REGION_ROUTE116/WEST -> REGION_ROUTE116/WEST_ABOVE_LEDGE"),
- hm_rules["HM01 Cut"]
+ entrance_rules["REGION_ROUTE116/WEST -> REGION_ROUTE116/WEST_ABOVE_LEDGE"] = hm_rules["HM01 Cut"]
+ entrance_rules["REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_116_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_116_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_116_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_116_2", "EVENT_DEFEAT_SHELLY")
)
# Rusturf Tunnel
- set_rule(
- get_entrance("REGION_RUSTURF_TUNNEL/WEST -> REGION_RUSTURF_TUNNEL/EAST"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_RUSTURF_TUNNEL/EAST -> REGION_RUSTURF_TUNNEL/WEST"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_HM_STRENGTH"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_location("EVENT_RECOVER_DEVON_GOODS"),
- lambda state: state.has("EVENT_DEFEAT_ROXANNE", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_HM_STRENGTH"] = hm_rules["HM06 Rock Smash"]
+ location_rules["EVENT_RECOVER_DEVON_GOODS"] = Has("EVENT_DEFEAT_ROXANNE")
+
+ entrance_rules["REGION_RUSTURF_TUNNEL/WEST -> REGION_RUSTURF_TUNNEL/EAST"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_RUSTURF_TUNNEL/EAST -> REGION_RUSTURF_TUNNEL/WEST"] = hm_rules["HM06 Rock Smash"]
# Route 115
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SEA"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SEA"),
- hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEHIND_ROCK"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"] = Has("Mach Bike")
+ entrance_rules["REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_115_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEHIND_ROCK"),
- hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_115_2", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/SEA"),
- hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"] = (
+ (OptionFilter(ExtraBoulders, ExtraBoulders.option_true) & hm_rules["HM04 Strength"]) |
+ (OptionFilter(ExtraBoulders, ExtraBoulders.option_false))
)
- set_rule(
- get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"),
- has_mach_bike
+ entrance_rules["REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE"] = (
+ (OptionFilter(ExtraBoulders, ExtraBoulders.option_true) & hm_rules["HM04 Strength"]) |
+ (OptionFilter(ExtraBoulders, ExtraBoulders.option_false))
)
- set_rule(
- get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_115_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_115_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"] = (
+ OptionFilter(ExtraBumpySlope, ExtraBumpySlope.option_true) & Has("Acro Bike")
)
- if world.options.extra_boulders:
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE"),
- hm_rules["HM04 Strength"]
- )
-
- if world.options.extra_bumpy_slope:
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
- has_acro_bike
- )
- else:
- set_rule(
- get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
- lambda state: False
- )
-
# Route 105
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_105_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_105_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_105_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"),
- lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_105_2", "EVENT_DEFEAT_SHELLY")
)
+ entrance_rules["MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"] = Has("EVENT_UNDO_REGI_SEAL")
# Route 106
- set_rule(
- get_entrance("REGION_ROUTE106/EAST -> REGION_ROUTE106/SEA"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE106/WEST -> REGION_ROUTE106/SEA"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_ROUTE106/EAST -> REGION_ROUTE106/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE106/WEST -> REGION_ROUTE106/SEA"] = hm_rules["HM03 Surf"]
# Dewford Town
- entrance = get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH")
- set_rule(
- entrance,
- lambda state:
- state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
- and state.has("EVENT_TALK_TO_MR_STONE", world.player)
- and state.has("EVENT_DELIVER_LETTER", world.player)
+ entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"] = (
+ CanReachEntrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN") &
+ HasAll("EVENT_TALK_TO_MR_STONE", "EVENT_DELIVER_LETTER")
)
- world.multiworld.register_indirect_condition(
- get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance)
- set_rule(
- get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"),
- lambda state:
- state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
- and state.has("EVENT_TALK_TO_MR_STONE", world.player)
- )
- set_rule(
- get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_DEWFORD_TOWN/WATER"),
- hm_rules["HM03 Surf"]
+ entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"] = (
+ CanReachEntrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN") &
+ Has("EVENT_TALK_TO_MR_STONE")
)
+ entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_DEWFORD_TOWN/WATER"] = hm_rules["HM03 Surf"]
# Granite Cave
- set_rule(
- get_entrance("REGION_GRANITE_CAVE_STEVENS_ROOM/MAIN -> REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED"),
- lambda state: state.has("Letter", world.player)
- )
- set_rule(
- get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"),
- has_mach_bike
- )
+ entrance_rules["REGION_GRANITE_CAVE_STEVENS_ROOM/MAIN -> REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED"] = Has("Letter")
+ entrance_rules["REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"] = Has("Mach Bike")
# Route 107
- set_rule(
- get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE107/MAIN"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE107/MAIN"] = hm_rules["HM03 Surf"]
# Route 109
- entrance = get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN")
- set_rule(
- entrance,
- lambda state:
- state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
- and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player)
- and state.has("EVENT_TALK_TO_MR_STONE", world.player)
- and state.has("EVENT_DELIVER_LETTER", world.player)
- )
- world.multiworld.register_indirect_condition(
- get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance)
- set_rule(
- get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"),
- hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"] = (
+ CanReachEntrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN") &
+ CanReachEntrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH") &
+ HasAll("EVENT_TALK_TO_MR_STONE", "EVENT_DELIVER_LETTER")
)
+ entrance_rules["REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"] = hm_rules["HM03 Surf"]
# Slateport City
- set_rule(
- get_entrance("REGION_SLATEPORT_CITY/MAIN -> REGION_SLATEPORT_CITY/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_location("EVENT_TALK_TO_DOCK"),
- lambda state: state.has("Devon Goods", world.player)
- )
- set_rule(
- get_entrance("MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1"),
- lambda state: state.has("EVENT_TALK_TO_DOCK", world.player)
- )
- set_rule(
- get_location("EVENT_AQUA_STEALS_SUBMARINE"),
- lambda state: state.has("EVENT_RELEASE_GROUDON", world.player)
- )
- set_rule(
- get_entrance("REGION_SLATEPORT_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"),
- lambda state: state.has("S.S. Ticket", world.player)
- )
+ location_rules["EVENT_TALK_TO_DOCK"] = Has("Devon Goods")
+ location_rules["EVENT_AQUA_STEALS_SUBMARINE"] = Has("EVENT_RELEASE_GROUDON")
+
+ entrance_rules["REGION_SLATEPORT_CITY/MAIN -> REGION_SLATEPORT_CITY/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1"] = Has("EVENT_TALK_TO_DOCK")
+ entrance_rules["REGION_SLATEPORT_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"] = Has("S.S. Ticket")
# Route 110
- set_rule(
- get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/NORTH_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST"),
- lambda state: has_acro_bike(state) or has_mach_bike(state)
+ entrance_rules["REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE110/MAIN -> REGION_ROUTE110/NORTH_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST"] = HasAny("Acro Bike", "Mach Bike")
+ entrance_rules["REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST"] = HasAny("Acro Bike", "Mach Bike")
+ entrance_rules["REGION_ROUTE110/SOUTH -> REGION_ROUTE110/MAIN"] = (
+ OptionFilter(RemoveRoadblocks, "Route 110 Aqua Grunts", "contains") | Has("EVENT_RESCUE_CAPT_STERN")
)
- set_rule(
- get_entrance("REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST"),
- lambda state: has_acro_bike(state) or has_mach_bike(state)
+ entrance_rules["REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH"] = (
+ OptionFilter(RemoveRoadblocks, "Route 110 Aqua Grunts", "contains") | Has("EVENT_RESCUE_CAPT_STERN")
)
- if "Route 110 Aqua Grunts" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("REGION_ROUTE110/SOUTH -> REGION_ROUTE110/MAIN"),
- lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH"),
- lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player)
- )
# Trick House
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS"),
- hm_rules["HM01 Cut"]
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE"),
- lambda state: state.has("Dynamo Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_1", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE"),
- lambda state: state.has("Heat Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_2", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE"),
- lambda state: state.has("Balance Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_3", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE"),
- lambda state: state.has("Feather Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_4", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE"),
- lambda state: state.has("Mind Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_5", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE"),
- lambda state: state.has("Rain Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_6", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_7", world.player)
- )
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS"] = hm_rules["HM01 Cut"]
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE"] = HasAll("Dynamo Badge", "EVENT_COMPLETE_TRICK_HOUSE_1")
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE"] = HasAll("Heat Badge", "EVENT_COMPLETE_TRICK_HOUSE_2")
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE"] = HasAll("Balance Badge", "EVENT_COMPLETE_TRICK_HOUSE_3")
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE"] = HasAll("Feather Badge", "EVENT_COMPLETE_TRICK_HOUSE_4")
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE"] = HasAll("Mind Badge", "EVENT_COMPLETE_TRICK_HOUSE_5")
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE"] = HasAll("Rain Badge", "EVENT_COMPLETE_TRICK_HOUSE_6")
+ entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE"] = HasAll("EVENT_DEFEAT_CHAMPION", "EVENT_COMPLETE_TRICK_HOUSE_7")
# Mauville City
- set_rule(
- get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"),
- lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_COIN_CASE"),
- lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player)
- )
+ location_rules["NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"] = Has("EVENT_DEFEAT_NORMAN")
+ location_rules["NPC_GIFT_RECEIVED_COIN_CASE"] = Has("EVENT_BUY_HARBOR_MAIL")
# Route 117
- set_rule(
- get_entrance("REGION_ROUTE117/MAIN -> REGION_ROUTE117/PONDS"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_location("EVENT_ENCOUNTER_LATIOS"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
+ location_rules["EVENT_ENCOUNTER_LATIOS"] = Has("EVENT_DEFEAT_CHAMPION")
+
+ entrance_rules["REGION_ROUTE117/MAIN -> REGION_ROUTE117/PONDS"] = hm_rules["HM03 Surf"]
# Route 111
- set_rule(
- get_entrance("REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/DESERT"),
- lambda state: state.has("Go Goggles", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE111/NORTH -> REGION_ROUTE111/DESERT"),
- lambda state: state.has("Go Goggles", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE111/NORTH -> REGION_ROUTE111/ABOVE_SLOPE"),
- has_mach_bike
- )
- set_rule(
- get_entrance("REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/SOUTH"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_ROUTE111/SOUTH -> REGION_ROUTE111/SOUTH_POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE111/SOUTH -> REGION_ROUTE111/MIDDLE"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
- set_rule(
- get_entrance("MAP_ROUTE111:1/MAP_DESERT_RUINS:0"),
- lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player)
- )
- set_rule(
- get_entrance("MAP_DESERT_RUINS:0/MAP_ROUTE111:1"),
- hm_rules["HM06 Rock Smash"]
- )
+ entrance_rules["REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/DESERT"] = Has("Go Goggles")
+ entrance_rules["REGION_ROUTE111/NORTH -> REGION_ROUTE111/DESERT"] = Has("Go Goggles")
+ entrance_rules["REGION_ROUTE111/NORTH -> REGION_ROUTE111/ABOVE_SLOPE"] = Has("Mach Bike")
+ entrance_rules["REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/SOUTH"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_ROUTE111/SOUTH -> REGION_ROUTE111/SOUTH_POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE111/SOUTH -> REGION_ROUTE111/MIDDLE"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0"] = Has("EVENT_DEFEAT_CHAMPION")
+ entrance_rules["MAP_ROUTE111:1/MAP_DESERT_RUINS:0"] = Has("EVENT_UNDO_REGI_SEAL")
+ entrance_rules["MAP_DESERT_RUINS:0/MAP_ROUTE111:1"] = hm_rules["HM06 Rock Smash"]
# Route 112
- if "Route 112 Magma Grunts" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("REGION_ROUTE112/SOUTH_EAST -> REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE"),
- lambda state: state.has("EVENT_MAGMA_STEALS_METEORITE", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE -> REGION_ROUTE112/SOUTH_EAST"),
- lambda state: state.has("EVENT_MAGMA_STEALS_METEORITE", world.player)
- )
+ entrance_rules["REGION_ROUTE112/SOUTH_EAST -> REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE"] = (
+ OptionFilter(RemoveRoadblocks, "Route 112 Magma Grunts", "contains") | Has("EVENT_MAGMA_STEALS_METEORITE")
+ )
+ entrance_rules["REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE -> REGION_ROUTE112/SOUTH_EAST"] = (
+ OptionFilter(RemoveRoadblocks, "Route 112 Magma Grunts", "contains") | Has("EVENT_MAGMA_STEALS_METEORITE")
+ )
# Fiery Path
- set_rule(
- get_entrance("REGION_FIERY_PATH/MAIN -> REGION_FIERY_PATH/BEHIND_BOULDER"),
- hm_rules["HM04 Strength"]
- )
+ entrance_rules["REGION_FIERY_PATH/MAIN -> REGION_FIERY_PATH/BEHIND_BOULDER"] = hm_rules["HM04 Strength"]
# Route 114
- set_rule(
- get_entrance("REGION_ROUTE114/MAIN -> REGION_ROUTE114/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE114/WATER -> REGION_ROUTE114/ABOVE_WATERFALL"),
- hm_rules["HM07 Waterfall"]
- )
- set_rule(
- get_entrance("MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ entrance_rules["REGION_ROUTE114/MAIN -> REGION_ROUTE114/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE114/WATER -> REGION_ROUTE114/ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0"] = Has("EVENT_DEFEAT_CHAMPION")
+ entrance_rules["REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_114_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_114_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_114_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_114_2", "EVENT_DEFEAT_SHELLY")
)
# Meteor Falls
- set_rule(
- get_entrance("REGION_METEOR_FALLS_1F_1R/MAIN -> REGION_METEOR_FALLS_1F_1R/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_1F_1R/WATER -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"),
- hm_rules["HM07 Waterfall"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT -> REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT -> REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER -> REGION_METEOR_FALLS_B1F_1R/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_METEOR_FALLS_B1F_2R/ENTRANCE -> REGION_METEOR_FALLS_B1F_2R/WATER"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_METEOR_FALLS_1F_1R/MAIN -> REGION_METEOR_FALLS_1F_1R/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_METEOR_FALLS_1F_1R/WATER -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"] = hm_rules["HM03 Surf"]
+ entrance_rules["MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0"] = Has("EVENT_DEFEAT_CHAMPION")
+ entrance_rules["REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT -> REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT -> REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER -> REGION_METEOR_FALLS_B1F_1R/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_METEOR_FALLS_B1F_2R/ENTRANCE -> REGION_METEOR_FALLS_B1F_2R/WATER"] = hm_rules["HM03 Surf"]
# Jagged Pass
- set_rule(
- get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"),
- has_acro_bike
- )
- set_rule(
- get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"),
- has_acro_bike
- )
- set_rule(
- get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"),
- lambda state: state.has("Magma Emblem", world.player)
- )
+ entrance_rules["REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"] = Has("Acro Bike")
+ entrance_rules["REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"] = Has("Acro Bike")
+ entrance_rules["MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"] = Has("Magma Emblem")
# Lavaridge Town
- set_rule(
- get_location("NPC_GIFT_RECEIVED_GO_GOGGLES"),
- lambda state: state.has("EVENT_DEFEAT_FLANNERY", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_GO_GOGGLES"] = Has("EVENT_DEFEAT_FLANNERY")
# Mirage Tower
- set_rule(
- get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"),
- has_mach_bike
- )
- set_rule(
- get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"),
- has_mach_bike
- )
- set_rule(
- get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_MIRAGE_TOWER_3F/BOTTOM -> REGION_MIRAGE_TOWER_3F/TOP"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_MIRAGE_TOWER_4F/MAIN -> REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM"),
- hm_rules["HM06 Rock Smash"]
- )
+ entrance_rules["REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"] = Has("Mach Bike")
+ entrance_rules["REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"] = Has("Mach Bike")
+ entrance_rules["REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_MIRAGE_TOWER_3F/BOTTOM -> REGION_MIRAGE_TOWER_3F/TOP"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_MIRAGE_TOWER_4F/MAIN -> REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM"] = hm_rules["HM06 Rock Smash"]
# Abandoned Ship
- set_rule(
- get_entrance("REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER -> REGION_ABANDONED_SHIP_UNDERWATER1/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN -> REGION_ABANDONED_SHIP_UNDERWATER2/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0"),
- lambda state: state.has("Room 1 Key", world.player)
- )
- set_rule(
- get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2"),
- lambda state: state.has("Room 2 Key", world.player)
- )
- set_rule(
- get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6"),
- lambda state: state.has("Room 4 Key", world.player)
- )
- set_rule(
- get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8"),
- lambda state: state.has("Room 6 Key", world.player)
- )
- set_rule(
- get_entrance("MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0"),
- lambda state: state.has("Storage Key", world.player)
- )
+ entrance_rules["REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER -> REGION_ABANDONED_SHIP_UNDERWATER1/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN -> REGION_ABANDONED_SHIP_UNDERWATER2/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0"] = Has("Room 1 Key")
+ entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2"] = Has("Room 2 Key")
+ entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6"] = Has("Room 4 Key")
+ entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8"] = Has("Room 6 Key")
+ entrance_rules["MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0"] = Has("Storage Key")
# New Mauville
- set_rule(
- get_entrance("MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0"),
- lambda state: state.has("Basement Key", world.player)
- )
+ entrance_rules["MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0"] = Has("Basement Key")
# Route 118
- if world.options.modify_118:
- set_rule(
- get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"),
- has_acro_bike
- )
- set_rule(
- get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"),
- has_acro_bike
- )
- set_rule(
- get_entrance("REGION_ROUTE118/WEST_WATER -> REGION_ROUTE118/EAST_WATER"),
- lambda state: False
- )
- set_rule(
- get_entrance("REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/WEST_WATER"),
- lambda state: False
- )
- else:
- set_rule(
- get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"),
- lambda state: False
- )
- set_rule(
- get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"),
- lambda state: False
- )
-
- set_rule(
- get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/WEST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/EAST_WATER"),
- hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"] = (
+ OptionFilter(ModifyRoute118, ModifyRoute118.option_true) & Has("Acro Bike")
)
- set_rule(
- get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_118_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"] = (
+ OptionFilter(ModifyRoute118, ModifyRoute118.option_true) & Has("Acro Bike")
)
- set_rule(
- get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("TERRA_CAVE_ROUTE_118_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_ROUTE118/WEST_WATER -> REGION_ROUTE118/EAST_WATER"] = (
+ OptionFilter(ModifyRoute118, ModifyRoute118.option_false) & True_()
)
-
- # Route 119
- set_rule(
- get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"),
- has_acro_bike
+ entrance_rules["REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/WEST_WATER"] = (
+ OptionFilter(ModifyRoute118, ModifyRoute118.option_false) & True_()
)
- set_rule(
- get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"),
- has_acro_bike
+ entrance_rules["REGION_ROUTE118/WEST -> REGION_ROUTE118/WEST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE118/EAST -> REGION_ROUTE118/EAST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_118_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"),
- hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = (
+ HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_118_2", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE119/MIDDLE_RIVER -> REGION_ROUTE119/ABOVE_WATERFALL"),
- hm_rules["HM07 Waterfall"]
- )
- set_rule(
- get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/MIDDLE_RIVER"),
- hm_rules["HM07 Waterfall"]
+
+ # Route 119
+ entrance_rules["REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"] = Has("Acro Bike")
+ entrance_rules["REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"] = Has("Acro Bike")
+ entrance_rules["REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE119/MIDDLE_RIVER -> REGION_ROUTE119/ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/MIDDLE_RIVER"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"] = Has("Acro Bike")
+ entrance_rules["REGION_ROUTE119/MIDDLE -> REGION_ROUTE119/UPPER"] = (
+ OptionFilter(RemoveRoadblocks, "Route 119 Aqua Grunts", "contains") | Has("EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"),
- has_acro_bike
+ entrance_rules["REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE"] = (
+ OptionFilter(RemoveRoadblocks, "Route 119 Aqua Grunts", "contains") | Has("EVENT_DEFEAT_SHELLY")
)
- if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("REGION_ROUTE119/MIDDLE -> REGION_ROUTE119/UPPER"),
- lambda state: state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE"),
- lambda state: state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
# Fortree City
- set_rule(
- get_entrance("REGION_FORTREE_CITY/MAIN -> REGION_FORTREE_CITY/BEFORE_GYM"),
- lambda state: state.has("Devon Scope", world.player)
- )
- set_rule(
- get_entrance("REGION_FORTREE_CITY/BEFORE_GYM -> REGION_FORTREE_CITY/MAIN"),
- lambda state: state.has("Devon Scope", world.player)
- )
+ entrance_rules["REGION_FORTREE_CITY/MAIN -> REGION_FORTREE_CITY/BEFORE_GYM"] = Has("Devon Scope")
+ entrance_rules["REGION_FORTREE_CITY/BEFORE_GYM -> REGION_FORTREE_CITY/MAIN"] = Has("Devon Scope")
# Route 120
- set_rule(
- get_entrance("REGION_ROUTE120/NORTH -> REGION_ROUTE120/NORTH_POND_SHORE"),
- lambda state: state.has("Devon Scope", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH"),
- lambda state: state.has("Devon Scope", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_ALCOVE"),
- hm_rules["HM01 Cut"]
- )
- set_rule(
- get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE120/SOUTH_ALCOVE -> REGION_ROUTE120/SOUTH"),
- hm_rules["HM01 Cut"]
- )
- set_rule(
- get_entrance("MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"),
- lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player)
- )
- set_rule(
- get_entrance("MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2"),
- hm_rules["HM05 Flash"]
- )
+ entrance_rules["REGION_ROUTE120/NORTH -> REGION_ROUTE120/NORTH_POND_SHORE"] = Has("Devon Scope")
+ entrance_rules["REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH"] = Has("Devon Scope")
+ entrance_rules["REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_ALCOVE"] = hm_rules["HM01 Cut"]
+ entrance_rules["REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE120/SOUTH_ALCOVE -> REGION_ROUTE120/SOUTH"] = hm_rules["HM01 Cut"]
+ entrance_rules["MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"] = Has("EVENT_UNDO_REGI_SEAL")
+ entrance_rules["MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2"] = hm_rules["HM05 Flash"]
# Route 121
- set_rule(
- get_entrance("REGION_ROUTE121/EAST -> REGION_ROUTE121/WEST"),
- hm_rules["HM01 Cut"]
- )
- set_rule(
- get_entrance("REGION_ROUTE121/EAST -> REGION_ROUTE121/WATER"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_ROUTE121/EAST -> REGION_ROUTE121/WEST"] = hm_rules["HM01 Cut"]
+ entrance_rules["REGION_ROUTE121/EAST -> REGION_ROUTE121/WATER"] = hm_rules["HM03 Surf"]
# Safari Zone
- set_rule(
- get_entrance("MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0"),
- lambda state: state.has("Pokeblock Case", world.player)
- )
- set_rule(
- get_entrance("REGION_SAFARI_ZONE_NORTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/POND"),
- hm_rules["HM03 Surf"]
+ entrance_rules["MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0"] = Has("Pokeblock Case")
+ entrance_rules["REGION_SAFARI_ZONE_NORTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"] = Has("Acro Bike")
+ entrance_rules["REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"] = Has("Mach Bike")
+ entrance_rules["REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SAFARI_ZONE_SOUTHEAST/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/MAIN"] = (
+ OptionFilter(RemoveRoadblocks, "Safari Zone Construction Workers", "contains") | Has("EVENT_DEFEAT_CHAMPION")
)
- set_rule(
- get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"),
- has_acro_bike
- )
- set_rule(
- get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"),
- has_mach_bike
- )
- set_rule(
- get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SAFARI_ZONE_SOUTHEAST/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/WATER"),
- hm_rules["HM03 Surf"]
- )
- if "Safari Zone Construction Workers" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
- )
# Route 122
- set_rule(
- get_entrance("REGION_ROUTE122/MT_PYRE_ENTRANCE -> REGION_ROUTE122/SEA"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_ROUTE122/MT_PYRE_ENTRANCE -> REGION_ROUTE122/SEA"] = hm_rules["HM03 Surf"]
# Route 123
- set_rule(
- get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE122/SEA"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE123/POND"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE123/EAST_BEHIND_TREE"),
- hm_rules["HM01 Cut"]
- )
+ entrance_rules["REGION_ROUTE123/EAST -> REGION_ROUTE122/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE123/EAST -> REGION_ROUTE123/POND"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_ROUTE123/EAST -> REGION_ROUTE123/EAST_BEHIND_TREE"] = hm_rules["HM01 Cut"]
# Lilycove City
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY/MAIN -> REGION_LILYCOVE_CITY/SEA"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"),
- lambda state: state.has("S.S. Ticket", world.player)
+ entrance_rules["REGION_LILYCOVE_CITY/MAIN -> REGION_LILYCOVE_CITY/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"] = Has("S.S. Ticket")
+ entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN"] = Has("Eon Ticket")
+ entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_FARAWAY_ISLAND_ENTRANCE/MAIN"] = Has("Old Sea Map")
+ entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_BIRTH_ISLAND_HARBOR/MAIN"] = Has("Aurora Ticket")
+ entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_NAVEL_ROCK_HARBOR/MAIN"] = Has("Mystic Ticket")
+ entrance_rules["REGION_LILYCOVE_CITY/SEA -> REGION_ROUTE124/MAIN"] = (
+ OptionFilter(RemoveRoadblocks, "Lilycove City Wailmer", "contains") | Has("EVENT_CLEAR_AQUA_HIDEOUT")
)
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN"),
- lambda state: state.has("Eon Ticket", world.player)
- )
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_FARAWAY_ISLAND_ENTRANCE/MAIN"),
- lambda state: state.has("Old Sea Map", world.player)
- )
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_BIRTH_ISLAND_HARBOR/MAIN"),
- lambda state: state.has("Aurora Ticket", world.player)
- )
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_NAVEL_ROCK_HARBOR/MAIN"),
- lambda state: state.has("Mystic Ticket", world.player)
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_LILYCOVE_CITY/SEA"] = (
+ OptionFilter(RemoveRoadblocks, "Lilycove City Wailmer", "contains") | Has("EVENT_CLEAR_AQUA_HIDEOUT")
)
- if "Lilycove City Wailmer" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("REGION_LILYCOVE_CITY/SEA -> REGION_ROUTE124/MAIN"),
- lambda state: state.has("EVENT_CLEAR_AQUA_HIDEOUT", world.player)
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_LILYCOVE_CITY/SEA"),
- lambda state: state.has("EVENT_CLEAR_AQUA_HIDEOUT", world.player)
- )
-
# Magma Hideout
- set_rule(
- get_entrance("REGION_MAGMA_HIDEOUT_1F/ENTRANCE -> REGION_MAGMA_HIDEOUT_1F/MAIN"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_MAGMA_HIDEOUT_1F/MAIN -> REGION_MAGMA_HIDEOUT_1F/ENTRANCE"),
- hm_rules["HM04 Strength"]
- )
+ entrance_rules["REGION_MAGMA_HIDEOUT_1F/ENTRANCE -> REGION_MAGMA_HIDEOUT_1F/MAIN"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_MAGMA_HIDEOUT_1F/MAIN -> REGION_MAGMA_HIDEOUT_1F/ENTRANCE"] = hm_rules["HM04 Strength"]
# Aqua Hideout
- if "Aqua Hideout Grunts" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("REGION_AQUA_HIDEOUT_1F/WATER -> REGION_AQUA_HIDEOUT_1F/MAIN"),
- lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
- )
- set_rule(
- get_entrance("REGION_AQUA_HIDEOUT_1F/MAIN -> REGION_AQUA_HIDEOUT_1F/WATER"),
- lambda state: hm_rules["HM03 Surf"](state) and state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
- )
-
- # Route 124
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/BIG_AREA"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_1"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_2"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_3"),
- hm_rules["HM08 Dive"]
+ entrance_rules["REGION_AQUA_HIDEOUT_1F/WATER -> REGION_AQUA_HIDEOUT_1F/MAIN"] = (
+ OptionFilter(RemoveRoadblocks, "Aqua Hideout Grunts", "contains") | Has("EVENT_AQUA_STEALS_SUBMARINE")
)
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"),
- hm_rules["HM08 Dive"]
+ entrance_rules["REGION_AQUA_HIDEOUT_1F/MAIN -> REGION_AQUA_HIDEOUT_1F/WATER"] = (
+ OptionFilter(RemoveRoadblocks, "Aqua Hideout Grunts", "contains") |
+ (hm_rules["HM03 Surf"] & Has("EVENT_AQUA_STEALS_SUBMARINE"))
)
+ # Route 124
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/BIG_AREA"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_1"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_2"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_3"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/NORTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/NORTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/NORTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"] = hm_rules["HM08 Dive"]
+
# Mossdeep City
- set_rule(
- get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_MOSSDEEP_CITY/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE124/MAIN"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE125/SEA"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE127/MAIN"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_location("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION"),
- lambda state: state.has("EVENT_DEFEAT_TATE_AND_LIZA", world.player)
- )
- set_rule(
- get_location("EVENT_STEVEN_GIVES_DIVE"),
- lambda state: state.has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_HM_DIVE"),
- lambda state: state.has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION", world.player)
- )
+ location_rules["EVENT_DEFEAT_MAXIE_AT_SPACE_STATION"] = Has("EVENT_DEFEAT_TATE_AND_LIZA")
+ location_rules["EVENT_STEVEN_GIVES_DIVE"] = Has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION")
+ location_rules["NPC_GIFT_RECEIVED_HM_DIVE"] = Has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION")
+
+ entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_MOSSDEEP_CITY/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE124/MAIN"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE125/SEA"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE127/MAIN"] = hm_rules["HM03 Surf"]
# Route 125
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_125_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_125_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_125_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_125_2", "EVENT_DEFEAT_SHELLY")
)
# Shoal Cave
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_EAST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST"),
- hm_rules["HM04 Strength"]
- )
+ entrance_rules["REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_EAST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST"] = hm_rules["HM04 Strength"]
# Route 126
- set_rule(
- get_entrance("REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_2"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE126/NEAR_ROUTE_124 -> REGION_UNDERWATER_ROUTE126/TUNNEL"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE126/NORTH_WEST_CORNER -> REGION_UNDERWATER_ROUTE126/TUNNEL"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_1"),
- hm_rules["HM08 Dive"]
- )
+ entrance_rules["REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_2"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE126/NEAR_ROUTE_124 -> REGION_UNDERWATER_ROUTE126/TUNNEL"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE126/NORTH_WEST_CORNER -> REGION_UNDERWATER_ROUTE126/TUNNEL"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_1"] = hm_rules["HM08 Dive"]
# Sootopolis City
- set_rule(
- get_entrance("REGION_SOOTOPOLIS_CITY/WATER -> REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_SOOTOPOLIS_CITY/EAST -> REGION_SOOTOPOLIS_CITY/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SOOTOPOLIS_CITY/WEST -> REGION_SOOTOPOLIS_CITY/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SOOTOPOLIS_CITY/ISLAND -> REGION_SOOTOPOLIS_CITY/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0"),
- lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player)
- )
- set_rule(
- get_entrance("MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0"),
- lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player)
- )
- set_rule(
- get_location("NPC_GIFT_RECEIVED_HM_WATERFALL"),
- lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player)
- )
- set_rule(
- get_location("EVENT_RAYQUAZA_STOPS_FIGHT"),
- lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_HM_WATERFALL"] = Has("EVENT_RAYQUAZA_STOPS_FIGHT")
+ location_rules["EVENT_RAYQUAZA_STOPS_FIGHT"] = Has("EVENT_RELEASE_KYOGRE")
+
+ entrance_rules["REGION_SOOTOPOLIS_CITY/WATER -> REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_SOOTOPOLIS_CITY/EAST -> REGION_SOOTOPOLIS_CITY/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SOOTOPOLIS_CITY/WEST -> REGION_SOOTOPOLIS_CITY/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SOOTOPOLIS_CITY/ISLAND -> REGION_SOOTOPOLIS_CITY/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0"] = Has("EVENT_RELEASE_KYOGRE")
+ entrance_rules["MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0"] = Has("EVENT_RAYQUAZA_STOPS_FIGHT")
# Route 127
- set_rule(
- get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/TUNNEL"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_1"),
- hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/TUNNEL"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_1"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_2"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_3"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE127/ENCLOSED_AREA -> REGION_UNDERWATER_ROUTE127/TUNNEL"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_127_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_2"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_3"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE127/ENCLOSED_AREA -> REGION_UNDERWATER_ROUTE127/TUNNEL"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_127_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
- )
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_127_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_127_2", "EVENT_DEFEAT_SHELLY")
)
# Route 128
- set_rule(
- get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_1"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_2"),
- hm_rules["HM08 Dive"]
- )
+ entrance_rules["REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_1"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_2"] = hm_rules["HM08 Dive"]
# Seafloor Cavern
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN -> REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER -> REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM1/NORTH"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM1/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/EAST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/EAST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM6/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM7/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM8/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH"),
- hm_rules["HM04 Strength"]
- )
- set_rule(
- get_entrance("REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM8/NORTH"),
- hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN -> REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER -> REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM1/NORTH"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM1/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/EAST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM6/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM7/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM8/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH"] = hm_rules["HM04 Strength"]
+ entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM8/NORTH"] = hm_rules["HM04 Strength"]
+ entrance_rules["MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0"] = (
+ OptionFilter(RemoveRoadblocks, "Seafloor Cavern Aqua Grunt", "contains") | Has("EVENT_STEVEN_GIVES_DIVE")
)
- if "Seafloor Cavern Aqua Grunt" not in world.options.remove_roadblocks.value:
- set_rule(
- get_entrance("MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0"),
- lambda state: state.has("EVENT_STEVEN_GIVES_DIVE", world.player)
- )
# Route 129
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_129_1", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_129_1", "EVENT_DEFEAT_SHELLY")
)
- set_rule(
- get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state)
- and state.has("EVENT_DEFEAT_CHAMPION", world.player)
- and state.has("MARINE_CAVE_ROUTE_129_2", world.player)
- and state.has("EVENT_DEFEAT_SHELLY", world.player)
+ entrance_rules["REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = (
+ hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_129_2", "EVENT_DEFEAT_SHELLY")
)
# Pacifidlog Town
- set_rule(
- get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_PACIFIDLOG_TOWN/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE131/MAIN"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE132/EAST"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_PACIFIDLOG_TOWN/MAIN -> REGION_PACIFIDLOG_TOWN/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE131/MAIN"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE132/EAST"] = hm_rules["HM03 Surf"]
# Sky Pillar
- set_rule(
- get_entrance("MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0"),
- lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player)
- )
- add_rule(
- get_location("EVENT_ENCOUNTER_RAYQUAZA"),
- lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player)
- )
- set_rule(
- get_entrance("REGION_SKY_PILLAR_2F/RIGHT -> REGION_SKY_PILLAR_2F/LEFT"),
- has_mach_bike
- )
- set_rule(
- get_entrance("REGION_SKY_PILLAR_2F/LEFT -> REGION_SKY_PILLAR_2F/RIGHT"),
- has_mach_bike
- )
- set_rule(
- get_entrance("REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"),
- has_mach_bike
- )
+ location_rules["EVENT_ENCOUNTER_RAYQUAZA"] = Has("EVENT_RAYQUAZA_STOPS_FIGHT")
+
+ entrance_rules["MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0"] = Has("EVENT_RELEASE_KYOGRE")
+ entrance_rules["REGION_SKY_PILLAR_2F/RIGHT -> REGION_SKY_PILLAR_2F/LEFT"] = Has("Mach Bike")
+ entrance_rules["REGION_SKY_PILLAR_2F/LEFT -> REGION_SKY_PILLAR_2F/RIGHT"] = Has("Mach Bike")
+ entrance_rules["REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"] = Has("Mach Bike")
# Route 134
- set_rule(
- get_entrance("REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"),
- hm_rules["HM08 Dive"]
- )
- set_rule(
- get_location("EVENT_UNDO_REGI_SEAL"),
- lambda state: state.has("CATCH_SPECIES_WAILORD", world.player) and state.has("CATCH_SPECIES_RELICANTH", world.player)
- )
- set_rule(
- get_entrance("REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"),
- lambda state: state.has("EVENT_MOVE_TUTOR_DIG", world.player)
- )
+ location_rules["EVENT_UNDO_REGI_SEAL"] = HasAll("CATCH_SPECIES_WAILORD", "CATCH_SPECIES_RELICANTH")
+
+ entrance_rules["REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"] = hm_rules["HM08 Dive"]
+ entrance_rules["REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"] = Has("EVENT_MOVE_TUTOR_DIG")
# Ever Grande City
- set_rule(
- get_entrance("REGION_EVER_GRANDE_CITY/SEA -> REGION_EVER_GRANDE_CITY/SOUTH"),
- hm_rules["HM07 Waterfall"]
- )
- set_rule(
- get_entrance("REGION_EVER_GRANDE_CITY/SOUTH -> REGION_EVER_GRANDE_CITY/SEA"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_EVER_GRANDE_CITY/SEA -> REGION_EVER_GRANDE_CITY/SOUTH"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["REGION_EVER_GRANDE_CITY/SOUTH -> REGION_EVER_GRANDE_CITY/SEA"] = hm_rules["HM03 Surf"]
# Victory Road
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_UPPER -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST"),
- hm_rules["HM06 Rock Smash"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_UPPER"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_EAST -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"),
- hm_rules["HM07 Waterfall"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"),
- hm_rules["HM07 Waterfall"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"),
- hm_rules["HM03 Surf"]
- )
+ entrance_rules["REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_UPPER -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST"] = hm_rules["HM06 Rock Smash"]
+ entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_UPPER"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_WEST -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_EAST -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"] = hm_rules["HM07 Waterfall"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"] = hm_rules["HM03 Surf"]
# Pokemon League
- if world.options.elite_four_requirement == EliteFourRequirement.option_badges:
- set_rule(
- get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
- lambda state: state.has_group_unique("Badge", world.player, world.options.elite_four_count.value)
- )
- else:
- set_rule(
- get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
- lambda state: defeated_n_gym_leaders(state, world.options.elite_four_count.value)
+ entrance_rules["REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"] = (
+ (
+ OptionFilter(EliteFourRequirement, EliteFourRequirement.option_badges) &
+ HasGroupUnique("Badge", world.options.elite_four_count.value)
+ ) |
+ (
+ OptionFilter(EliteFourRequirement, EliteFourRequirement.option_gyms) &
+ create_defeated_n_gym_leaders_rule(world.options.elite_four_count.value)
)
+ )
# Battle Frontier
- set_rule(
- get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK -> REGION_SS_TIDAL_CORRIDOR/MAIN"),
- lambda state: state.has("S.S. Ticket", world.player)
- )
- set_rule(
- get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE -> REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"),
- hm_rules["HM03 Surf"]
- )
- set_rule(
- get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"),
- lambda state: state.has("Wailmer Pail", world.player) and hm_rules["HM03 Surf"](state)
- )
- set_rule(
- get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"),
- lambda state: state.has("Wailmer Pail", world.player)
- )
- set_rule(
- get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"),
- hm_rules["HM07 Waterfall"]
- )
+ entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK -> REGION_SS_TIDAL_CORRIDOR/MAIN"] = Has("S.S. Ticket")
+ entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE -> REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"] = hm_rules["HM03 Surf"]
+ entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"] = Has("Wailmer Pail") & hm_rules["HM03 Surf"]
+ entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"] = Has("Wailmer Pail")
+ entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"]
# Pokedex Rewards
if world.options.dexsanity:
@@ -1551,183 +637,100 @@ def get_location(location: str):
if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species:
continue
- set_rule(
- get_location(f"Pokedex - {species.label}"),
- lambda state, species_name=species.name: state.has(f"CATCH_{species_name}", world.player)
- )
+ location_rules[f"Pokedex - {species.label}"] = Has(f"CATCH_{species.name}")
# Legendary hunt prevents Latios from being a wild spawn so the roamer
# can be tracked, and also guarantees that the roamer is a Latios.
if world.options.goal == Goal.option_legendary_hunt and \
data.constants["SPECIES_LATIOS"] in world.allowed_dexsanity_species:
- set_rule(
- get_location(f"Pokedex - Latios"),
- lambda state: state.has("EVENT_ENCOUNTER_LATIOS", world.player)
- )
+ location_rules[f"Pokedex - Latios"] = Has("EVENT_ENCOUNTER_LATIOS")
# Overworld Items
if world.options.overworld_items:
# Route 117
- set_rule(
- get_location("ITEM_ROUTE_117_REVIVE"),
- hm_rules["HM01 Cut"]
- )
+ location_rules["ITEM_ROUTE_117_REVIVE"] = hm_rules["HM01 Cut"]
# Route 114
- set_rule(
- get_location("ITEM_ROUTE_114_PROTEIN"),
- hm_rules["HM06 Rock Smash"]
- )
+ location_rules["ITEM_ROUTE_114_PROTEIN"] = hm_rules["HM06 Rock Smash"]
# Victory Road
- set_rule(
- get_location("ITEM_VICTORY_ROAD_B1F_FULL_RESTORE"),
- lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
- )
+ location_rules["ITEM_VICTORY_ROAD_B1F_FULL_RESTORE"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"]
# Hidden Items
if world.options.hidden_items:
# Route 120
- set_rule(
- get_location("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1"),
- hm_rules["HM01 Cut"]
- )
+ location_rules["HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1"] = hm_rules["HM01 Cut"]
# Route 121
- set_rule(
- get_location("HIDDEN_ITEM_ROUTE_121_NUGGET"),
- hm_rules["HM01 Cut"]
- )
+ location_rules["HIDDEN_ITEM_ROUTE_121_NUGGET"] = hm_rules["HM01 Cut"]
# NPC Gifts
if world.options.npc_gifts:
# Littleroot Town
- set_rule(
- get_location("NPC_GIFT_RECEIVED_AMULET_COIN"),
- lambda state: state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("Balance Badge", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_AMULET_COIN"] = HasAll("EVENT_TALK_TO_MR_STONE", "Balance Badge")
# Route 104
- set_rule(
- get_location("NPC_GIFT_RECEIVED_WHITE_HERB"),
- lambda state: state.has("Dynamo Badge", world.player) and state.has("EVENT_MEET_FLOWER_SHOP_OWNER", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_WHITE_HERB"] = HasAll("Dynamo Badge", "EVENT_MEET_FLOWER_SHOP_OWNER")
# Devon Corp
- set_rule(
- get_location("NPC_GIFT_RECEIVED_EXP_SHARE"),
- lambda state: state.has("EVENT_DELIVER_LETTER", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_EXP_SHARE"] = Has("EVENT_DELIVER_LETTER")
# Route 116
- set_rule(
- get_location("NPC_GIFT_RECEIVED_REPEAT_BALL"),
- lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_REPEAT_BALL"] = Has("EVENT_RESCUE_CAPT_STERN")
# Dewford Town
- set_rule(
- get_location("NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB"),
- lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB"] = Has("EVENT_DEFEAT_NORMAN")
# Slateport City
- set_rule(
- get_location("NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH"),
- lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
- and state.has("Scanner", world.player)
- and state.has("Mind Badge", world.player)
+ location_rules["NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH"] = (
+ HasAll("EVENT_AQUA_STEALS_SUBMARINE", "Scanner", "Mind Badge")
)
- set_rule(
- get_location("NPC_GIFT_RECEIVED_DEEP_SEA_SCALE"),
- lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
- and state.has("Scanner", world.player)
- and state.has("Mind Badge", world.player)
+ location_rules["NPC_GIFT_RECEIVED_DEEP_SEA_SCALE"] = (
+ HasAll("EVENT_AQUA_STEALS_SUBMARINE", "Scanner", "Mind Badge")
)
# Mauville City
- set_rule(
- get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"),
- lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player)
+ location_rules["NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"] = (
+ HasAll("EVENT_DEFEAT_NORMAN", "EVENT_TURN_OFF_GENERATOR")
)
# Fallarbor Town
- set_rule(
- get_location("NPC_GIFT_RECEIVED_TM_RETURN"),
- lambda state: state.has("EVENT_RECOVER_METEORITE", world.player) and state.has("Meteorite", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_TM_RETURN"] = HasAll("EVENT_RECOVER_METEORITE", "Meteorite")
# Fortree City
- set_rule(
- get_location("NPC_GIFT_RECEIVED_MENTAL_HERB"),
- lambda state: state.has("EVENT_WINGULL_QUEST_2", world.player)
- )
+ location_rules["NPC_GIFT_RECEIVED_MENTAL_HERB"] = Has("EVENT_WINGULL_QUEST_2")
# Add Itemfinder requirement to hidden items
if world.options.require_itemfinder:
for location in world.multiworld.get_locations(world.player):
assert isinstance(location, PokemonEmeraldLocation)
if location.key is not None and data.locations[location.key].category == LocationCategory.HIDDEN_ITEM:
- add_rule(
- location,
- lambda state: state.has("Itemfinder", world.player)
- )
+ location_rules[location.key] &= Has("Itemfinder")
# Add Flash requirements to dark caves
# Granite Cave
if world.options.require_flash in [DarkCavesRequireFlash.option_only_granite_cave, DarkCavesRequireFlash.option_both]:
- add_rule(
- get_entrance("MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1"),
- hm_rules["HM05 Flash"]
- )
+ entrance_rules["MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1"] &= hm_rules["HM05 Flash"]
# Victory Road
if world.options.require_flash in [DarkCavesRequireFlash.option_only_victory_road, DarkCavesRequireFlash.option_both]:
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0"),
- hm_rules["HM05 Flash"]
- )
- add_rule(
- get_entrance("MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3"),
- hm_rules["HM05 Flash"]
- )
+ entrance_rules["MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0"] &= hm_rules["HM05 Flash"]
+ entrance_rules["MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3"] &= hm_rules["HM05 Flash"]
+
+ for name, rule in entrance_rules.items():
+ world.set_rule(world.get_entrance(name), rule)
+
+ for name, rule in location_rules.items():
+ if name in data.locations:
+ name = data.locations[name].label
+ world.set_rule(world.get_location(name), rule)
diff --git a/worlds/pokemon_emerald/util.py b/worlds/pokemon_emerald/util.py
index 3215113075e9..268ba9bf5fac 100644
--- a/worlds/pokemon_emerald/util.py
+++ b/worlds/pokemon_emerald/util.py
@@ -100,7 +100,7 @@ def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str:
8: "Super Rod",
9: "Super Rod",
}[slot]
-
+
return {
EncounterType.LAND: 'Land',
EncounterType.WATER: 'Water',
diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py
index 0810ec5dc748..c8ecdeaefc57 100644
--- a/worlds/pokemon_rb/__init__.py
+++ b/worlds/pokemon_rb/__init__.py
@@ -629,7 +629,7 @@ def get_filler_item_name(self) -> str:
if (combined_traps > 0 and
self.random.randint(1, 100) <= self.options.trap_percentage.value):
return self.select_trap()
- banned_items = item_groups["Unique"]
+ banned_items = item_groups["Unique"].copy()
if (((not self.options.tea) or "Saffron City" not in [self.fly_map, self.town_map_fly_map])
and (not self.options.door_shuffle)):
# under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being
diff --git a/worlds/sm64ex/Items.py b/worlds/sm64ex/Items.py
index 6cd9b5eb461b..ed1922157247 100644
--- a/worlds/sm64ex/Items.py
+++ b/worlds/sm64ex/Items.py
@@ -12,7 +12,7 @@ class SM64ItemData(NamedTuple):
classification: ItemClassification = ItemClassification.progression
generic_item_data_table: dict[str, SM64ItemData] = {
- "Power Star": SM64ItemData(sm64ex_base_id + 0, ItemClassification.progression_skip_balancing),
+ "Power Star": SM64ItemData(sm64ex_base_id + 0, ItemClassification.progression_deprioritized_skip_balancing),
"Basement Key": SM64ItemData(sm64ex_base_id + 178),
"Second Floor Key": SM64ItemData(sm64ex_base_id + 179),
"Progressive Key": SM64ItemData(sm64ex_base_id + 180),
diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py
index 2a8206b0cbd2..796a07621799 100644
--- a/worlds/sm64ex/Regions.py
+++ b/worlds/sm64ex/Regions.py
@@ -79,6 +79,14 @@ class SM64Region(Region):
sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level }
sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets }
+# Levels with at least one star without a movement rule
+# Currently excluding WF, HMC, WDW, TTM, THI, TTC, and RR
+valid_move_randomizer_start_courses = [
+ "Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain",
+ "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
+ "Dire, Dire Docks", "Snowman's Land"
+]
+
def create_regions(multiworld: MultiWorld, options: SM64Options, player: int):
regSS = Region("Menu", player, multiworld, "Castle Area")
create_default_locs(regSS, locSS_table)
diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py
index 92ee61bda4a7..e97c2aacd00f 100644
--- a/worlds/sm64ex/Rules.py
+++ b/worlds/sm64ex/Rules.py
@@ -5,7 +5,8 @@
from .Locations import location_table
from .Options import SM64Options
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\
-sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
+sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances,\
+valid_move_randomizer_start_courses
from .Items import action_item_data_table
def shuffle_dict_keys(multiworld: MultiWorld, dictionary: dict) -> dict:
@@ -28,40 +29,33 @@ def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_r
def set_rules(multiworld: MultiWorld, options: SM64Options, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int):
randomized_level_to_paintings = sm64_level_to_paintings.copy()
randomized_level_to_secrets = sm64_level_to_secrets.copy()
- valid_move_randomizer_start_courses = [
- "Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain",
- "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
- "Dire, Dire Docks", "Snowman's Land"
- ] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR
- if options.area_rando >= 1: # Some randomization is happening, randomize Courses
- randomized_level_to_paintings = shuffle_dict_keys(multiworld,sm64_level_to_paintings)
- # If not shuffling later, ensure a valid start course on move randomizer
- if options.area_rando < 3 and move_rando_bitvec > 0:
- swapdict = randomized_level_to_paintings.copy()
- invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses}
- fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld)
- fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld)
-
- if options.area_rando == 2: # Randomize Secrets as well
+
+ if options.area_rando > options.area_rando.option_Off: # Some randomization is happening, randomize Courses
+ randomized_level_to_paintings = shuffle_dict_keys(multiworld, sm64_level_to_paintings)
+
+ if options.area_rando == options.area_rando.option_Courses_and_Secrets_Separate: # Randomize Secrets as well
randomized_level_to_secrets = shuffle_dict_keys(multiworld, sm64_level_to_secrets)
- randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets}
- if options.area_rando == 3: # Randomize Courses and Secrets in one pool
+
+ randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} # Concatenate courses and secrets for rest
+
+ if options.area_rando == options.area_rando.option_Courses_and_Secrets: # Randomize Courses and Secrets in one pool
randomized_entrances = shuffle_dict_keys(multiworld, randomized_entrances)
- # Guarantee first entrance is a course
- swapdict = randomized_entrances.copy()
- if move_rando_bitvec == 0:
- fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, multiworld)
- else:
- invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses}
- fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld)
- fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld)
- # Guarantee BITFS is not mapped to DDD
- fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, multiworld)
- # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
- if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
- fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, multiworld)
- else:
- fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, multiworld)
+
+ # Now, fix assignment if necessary
+ swapdict = randomized_entrances.copy()
+ if move_rando_bitvec == 0:
+ fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, multiworld)
+ else:
+ invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses}
+ fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld)
+ fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld)
+ # Guarantee BITFS is not mapped to DDD
+ fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, multiworld)
+ # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
+ if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
+ fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, multiworld)
+ else:
+ fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, multiworld)
# Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code
# Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise
diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py
index f1208d2059e0..138425e43d2c 100644
--- a/worlds/sm64ex/__init__.py
+++ b/worlds/sm64ex/__init__.py
@@ -139,7 +139,7 @@ def create_items(self):
if self.move_rando_bitvec & (1 << itemdata.code - double_jump_bitvec_offset)]
def generate_basic(self):
- if not (self.options.buddy_checks):
+ if not self.options.buddy_checks:
self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB"))
self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF"))
self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB"))
diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md
index 62669a65eb20..bb4040ae1dae 100644
--- a/worlds/sm64ex/docs/setup_en.md
+++ b/worlds/sm64ex/docs/setup_en.md
@@ -21,16 +21,16 @@ First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT I
It is extremely encouraged to use the default install directory!
Then continue to `Using the Launcher`
-*Linux Preparations*
+*Linux / SteamOS Preparations*
-You will need to install some dependencies before using the launcher.
-The launcher itself needs `qt6`, `patch` and `git`, and building the game requires `sdl2 glew cmake python make` (If you install `jsoncpp` as well, it will be linked dynamically).
-Then continue to `Using the Launcher`
+The easiest installation method is using the Flatpak.
+If using Flatpak, no dependencies or other preparations are necessary.
+Otherwise, install `qt6`, `patch`, `git`, `sdl2`, `glew`, `cmake`, `python` and `make`.
*Using the Launcher*
1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry.
-2. Scroll down, and download the zip file for your OS.
+2. Scroll down, and download the zip file for Windows, or the Flatpak for Linux / SteamOS.
3. Unpack the zip file in an empty folder.
4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps.
- Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually.
diff --git a/worlds/sm64ex/test/__init__.py b/worlds/sm64ex/test/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/sm64ex/test/bases.py b/worlds/sm64ex/test/bases.py
new file mode 100644
index 000000000000..b19a5c0f25f4
--- /dev/null
+++ b/worlds/sm64ex/test/bases.py
@@ -0,0 +1,8 @@
+from test.bases import WorldTestBase
+
+from .. import SM64World
+
+
+class SM64TestBase(WorldTestBase):
+ game = "Super Mario 64"
+ world: SM64World
diff --git a/worlds/sm64ex/test/test_access.py b/worlds/sm64ex/test/test_access.py
new file mode 100644
index 000000000000..32695da259ff
--- /dev/null
+++ b/worlds/sm64ex/test/test_access.py
@@ -0,0 +1,81 @@
+from .bases import SM64TestBase
+from .. import Options
+from ..Regions import sm64_entrances_to_level, sm64_level_to_entrances
+
+# Access to Locations/Areas/Entrances by Power Star count in star_cost
+class StarCostAccessTestBase(SM64TestBase):
+ run_default_tests = False
+ options = {
+ "progressive_keys": Options.ProgressiveKeys.option_false,
+ "enable_locked_paintings": Options.EnableLockedPaintings.option_false,
+ # Test for access would mean access to entrance/painting,
+ # not level itself for the sake of entrance rando.
+ "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets,
+ }
+
+ def test_BoB_entrance_access(self):
+ # Always accessible, no stars needed.
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ bob_entrance_id = self.world.area_connections[bob_level_id]
+ bob_entrance = sm64_level_to_entrances[bob_entrance_id]
+ self.assertTrue(self.can_reach_region(bob_entrance))
+
+ def test_MIPS1_access(self):
+ # Requires Basement Key and Power Stars calculated in star_costs["MIPS1Cost"]
+ self.assertFalse(self.can_reach_location("MIPS 1"))
+ self.collect([self.get_item_by_name("Basement Key")])
+ self.assertFalse(self.can_reach_location("MIPS 1"))
+ mips1_cost = self.world.star_costs["MIPS1Cost"]
+ self.collect([self.get_item_by_name("Power Star")] * mips1_cost)
+ self.assertTrue(self.can_reach_location("MIPS 1"))
+
+ def test_MIPS2_access(self):
+ # Requires Basement Key and Power Stars calculated in star_costs["MIPS2Cost"]
+ self.assertFalse(self.can_reach_location("MIPS 2"))
+ self.collect([self.get_item_by_name("Basement Key")])
+ self.assertFalse(self.can_reach_location("MIPS 2"))
+ mips2_cost = self.world.star_costs["MIPS2Cost"]
+ self.collect([self.get_item_by_name("Power Star")] * mips2_cost)
+ self.assertTrue(self.can_reach_location("MIPS 2"))
+
+ def test_BitDW_entrance_access(self):
+ # Requires Power Stars calculated in star_costs["FirstBowserDoorCost"]
+ bitdw_level_id = sm64_entrances_to_level["Bowser in the Dark World"]
+ bitdw_entrance_id = self.world.area_connections[bitdw_level_id]
+ bitdw_entrance = sm64_level_to_entrances[bitdw_entrance_id]
+ self.assertFalse(self.can_reach_region(bitdw_entrance))
+ bitdw_cost = self.world.star_costs["FirstBowserDoorCost"]
+ self.collect([self.get_item_by_name("Power Star")] * bitdw_cost)
+ self.assertTrue(self.can_reach_region(bitdw_entrance))
+
+ # Since BitFS is locked behind "DDD: Board Bowser's Sub", we just need to test DDD.
+ def test_DDD_entrance_access(self):
+ # Requires Basement Key and Power Stars calculated in star_costs["BasementDoorCost"]
+ ddd_level_id = sm64_entrances_to_level["Dire, Dire Docks"]
+ ddd_entrance_id = self.world.area_connections[ddd_level_id]
+ ddd_entrance = sm64_level_to_entrances[ddd_entrance_id]
+ self.assertFalse(self.can_reach_region(ddd_entrance))
+ self.collect([self.get_item_by_name("Basement Key")])
+ self.assertFalse(self.can_reach_region(ddd_entrance))
+ bitfs_cost = self.world.star_costs["BasementDoorCost"]
+ self.collect([self.get_item_by_name("Power Star")] * bitfs_cost)
+ self.assertTrue(self.can_reach_region(ddd_entrance))
+
+ def test_Floor3_access(self):
+ # Requires Second Floor Key and Power Stars calculated in star_costs["SecondFloorDoorCost"]
+ self.assertFalse(self.can_reach_region("Third Floor"))
+ self.collect([self.get_item_by_name("Second Floor Key")])
+ self.assertFalse(self.can_reach_region("Third Floor"))
+ floor3_cost = self.world.star_costs["StarsToFinish"]
+ self.collect([self.get_item_by_name("Power Star")] * floor3_cost)
+ self.assertTrue(self.can_reach_region("Third Floor"))
+
+ def test_BitS_entrance_access(self):
+ # Requires Second Floor Key and Power Stars calculated in star_costs["StarsToFinish"]
+ self.assertFalse(self.can_reach_region("Bowser in the Sky"))
+ self.collect([self.get_item_by_name("Second Floor Key")])
+ self.assertFalse(self.can_reach_region("Bowser in the Sky"))
+ bits_cost = self.world.star_costs["StarsToFinish"]
+ self.collect([self.get_item_by_name("Power Star")] * bits_cost)
+ self.assertTrue(self.can_reach_region("Third Floor"))
+ self.assertTrue(self.can_reach_region("Bowser in the Sky"))
diff --git a/worlds/sm64ex/test/test_options.py b/worlds/sm64ex/test/test_options.py
new file mode 100644
index 000000000000..2a55b31723b5
--- /dev/null
+++ b/worlds/sm64ex/test/test_options.py
@@ -0,0 +1,278 @@
+from .bases import SM64TestBase
+from .. import Options
+from ..Locations import loc100Coin_table, location_table
+from ..Regions import sm64_entrances_to_level, sm64_level_to_paintings, sm64_level_to_secrets, \
+ valid_move_randomizer_start_courses
+
+valid_move_randomizer_start_entrances = {
+ level: entrance
+ for (level, entrance) in sm64_entrances_to_level.items()
+ if level in valid_move_randomizer_start_courses
+}
+
+
+# Coin Star Logic
+class EnableCoinStarsTestBase(SM64TestBase):
+ options = {
+ "enable_coin_stars": Options.EnableCoinStars.option_on
+ }
+
+ # Ensure Coin Star locations are created
+ def test_coin_star_locations(self):
+ possible_locations = self.world.location_names
+ for loc in loc100Coin_table:
+ # Use subtest to force all locations to be tested
+ with self.subTest("Location created", location=loc):
+ self.assertIn(loc, possible_locations)
+
+
+class DisableCoinStarsTestBase(SM64TestBase):
+ options = {
+ "enable_coin_stars": Options.EnableCoinStars.option_off
+ }
+
+ # Ensure Coin Star locations are not created
+ def test_coin_star_locations(self):
+ possible_locations = self.world.get_locations()
+ for loc in loc100Coin_table:
+ # Use subtest to force all locations to be tested
+ with self.subTest("Location not created", location=loc):
+ self.assertNotIn(loc, possible_locations)
+
+
+class VanillaCoinStarsTestBase(SM64TestBase):
+ options = {
+ "enable_coin_stars": Options.EnableCoinStars.option_vanilla
+ }
+
+ # Ensure Coin Star locations are created
+ def test_coin_star_locations(self):
+ possible_locations = self.world.location_names
+ for loc in loc100Coin_table:
+ # Use subtest to force all locations to be tested
+ with self.subTest("Location created", location=loc):
+ self.assertIn(loc, possible_locations)
+
+ # Vanilla Coin Stars should give the player their own Power Stars
+ def test_items_in_coin_star_locations(self):
+ for loc in loc100Coin_table:
+ # Use subtest to force all locations to be tested
+ with self.subTest("Location created", location=loc):
+ item_in_loc = self.world.get_location(loc).item
+ self.assertEqual(item_in_loc.name, "Power Star")
+ # By default, these test bases are single player multiworld.
+ # In any other case, we should test that they belong to their respective worlds.
+
+
+# Exclamation Boxes
+class ExclamationBoxesOnTestBase(SM64TestBase):
+ options = {
+ "exclamation_boxes": Options.ExclamationBoxes.option_true,
+ }
+
+
+class ExclamationBoxesOffTestBase(SM64TestBase):
+ options = {
+ "exclamation_boxes": Options.ExclamationBoxes.option_false,
+ }
+
+ # Should populate the boxes with the players own 1Up Mushrooms
+ def test_items_in_exclamation_box_locations(self):
+ # Get 1Up Block locations
+ loc1ups_table = {name for name in location_table.keys() if "1Up Block" in name}
+ for loc in loc1ups_table:
+ # Use subtest to force all locations to be tested
+ with self.subTest("Location has own 1Up Mushroom.", location=loc):
+ item_in_loc = self.world.get_location(loc).item
+ self.assertEqual(item_in_loc.name, "1Up Mushroom")
+ # By default, these test bases are single player multiworld.
+ # In any other case, we should test that they belong to their respective worlds.
+
+
+# Entrance Randomizer
+class EntranceRandoOffTestBase(SM64TestBase):
+ options = {
+ "area_rando": Options.AreaRandomizer.option_Off
+ }
+
+ # Ensure entrance rando disabled
+ def test_BoB_entrance(self):
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ self.assertEqual(self.world.area_connections[bob_level_id], bob_level_id)
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id)
+
+
+class EntranceRandoCourseTestBase(SM64TestBase):
+ options = {
+ "area_rando": Options.AreaRandomizer.option_Courses_Only
+ }
+
+ def test_BoB_entrance(self):
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ # BoB goes to a painting, not a secret
+ self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
+ self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ # BitFS is a secret (aka not a course), unaffected by Course Only entrance rando.
+ self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id)
+
+
+class EntranceRandoSeparateTestBase(SM64TestBase):
+ options = {
+ "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets_Separate
+ }
+
+ def test_BoB_entrance(self):
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ # BoB goes to a painting, not a secret
+ self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
+ self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ # BitFS goes to a secret, not a painting
+ self.assertIn(self.world.area_connections[bitfs_level_id], sm64_level_to_secrets.keys())
+ self.assertNotIn(self.world.area_connections[bitfs_level_id], sm64_level_to_paintings.keys())
+ # BitFS does not go to DDD
+ self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
+
+
+class EntranceRandoAllTestBase(SM64TestBase):
+ options = {
+ "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets
+ }
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ # BitFS does not go to DDD
+ self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
+
+
+# Completion Type
+class CompletionLastBowserTestBase(SM64TestBase):
+ options = {
+ "completion_type": Options.CompletionType.option_Last_Bowser_Stage
+ }
+
+
+class CompletionAllBowserTestBase(SM64TestBase):
+ options = {
+ "completion_type": Options.CompletionType.option_All_Bowser_Stages
+ }
+
+
+# Option Combos
+
+
+# Smallest Power Star count possible
+class MinimumStarsPossibleTestBase(SM64TestBase):
+ options = {
+ "amount_of_stars": Options.AmountOfStars.range_start,
+ "enable_move_rando": Options.EnableMoveRandomizer.option_true,
+ "exclamation_boxes": Options.ExclamationBoxes.option_false,
+ "enable_coin_stars": Options.EnableCoinStars.option_off
+ }
+
+ # There will be less Power Stars than filler with this low of a star count
+ def test_stars_vs_filler(self):
+ filler_count = len(self.get_items_by_name("1Up Mushroom"))
+ star_count = len(self.get_items_by_name("Power Star"))
+ self.assertGreater(filler_count, star_count)
+
+
+# Entrance + Move Randos
+class CourseEntrancesMoveTestBase(SM64TestBase):
+ options = {
+ "enable_move_rando": Options.EnableMoveRandomizer.option_true,
+ "area_rando": Options.AreaRandomizer.option_Courses_Only
+ }
+
+ def test_BoB_entrance(self):
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ # BoB goes to a course, not a secret.
+ self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
+ self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
+ # BoB goes to level with at least one star without a movement rule.
+ self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values())
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ # BitFS is a secret (aka not a course), unaffected by Course Only entrance rando.
+ self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id)
+
+ def test_WF_entrance(self):
+ wf_level_id = sm64_entrances_to_level["Whomp's Fortress"]
+ # WF goes to level with at least one star without a movement rule.
+ self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values())
+
+
+class SeparateEntrancesMoveTestBase(SM64TestBase):
+ options = {
+ "enable_move_rando": Options.EnableMoveRandomizer.option_true,
+ "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets_Separate
+ }
+
+ def test_BoB_entrance(self):
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ # BoB goes to a course, not a secret.
+ self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
+ self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
+ # BoB goes to level with at least one star without a movement rule.
+ self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values())
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ # BitFS does not go to DDD.
+ self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
+
+ def test_WF_entrance(self):
+ wf_level_id = sm64_entrances_to_level["Whomp's Fortress"]
+ # WF goes to level with at least one star without a movement rule.
+ self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values())
+
+
+class AllEntrancesMoveTestBase(SM64TestBase):
+ options = {
+ "enable_move_rando": Options.EnableMoveRandomizer.option_true,
+ "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets
+ }
+
+ def test_BoB_entrance(self):
+ bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
+ # BoB goes to level with at least one star without a movement rule.
+ self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values())
+
+ def test_BitFS_entrance(self):
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ # BitFS does not go to DDD.
+ self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
+
+ def test_WF_entrance(self):
+ wf_level_id = sm64_entrances_to_level["Whomp's Fortress"]
+ # WF goes to level with at least one star without a movement rule.
+ self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values())
+
+ def test_CotMC_entrance(self):
+ cotmc_level_id = sm64_entrances_to_level["Cavern of the Metal Cap"]
+ # CotMC does not go to HMC.
+ self.assertIsNot(self.world.area_connections[cotmc_level_id], sm64_entrances_to_level["Hazy Maze Cave"])
+ # If BitFS -> HMC, CotMC does not go to DDD.
+ bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
+ if self.world.area_connections[bitfs_level_id] == sm64_entrances_to_level["Hazy Maze Cave"]:
+ self.assertIsNot(self.world.area_connections[cotmc_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
+
+
+# No Strict Requirements
+class NoStrictRequirementsTestBase(SM64TestBase):
+ options = {
+ "enable_move_rando": Options.EnableMoveRandomizer.option_true,
+ "buddy_checks": Options.BuddyChecks.option_true,
+ "strict_move_requirements": Options.StrictMoveRequirements.option_false,
+ "strict_cap_requirements": Options.StrictCapRequirements.option_false,
+ "strict_cannon_requirements": Options.StrictCannonRequirements.option_false,
+ }
diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py
index 3fe294a3baf5..dcf0a2bc7919 100644
--- a/worlds/stardew_valley/stardew_rule/state.py
+++ b/worlds/stardew_valley/stardew_rule/state.py
@@ -27,7 +27,7 @@ def __init__(self, count: int, items: Union[str, Iterable[str]], player: int):
def __call__(self, state: CollectionState) -> bool:
c = 0
for item in self.items:
- c += state.count(item, self.player)
+ c += state.prog_items[self.player][item]
if c >= self.count:
return True
return False
@@ -56,7 +56,7 @@ def value(self):
return self.count
def __call__(self, state: CollectionState) -> bool:
- return state.has(self.item, self.player, self.count)
+ return state.prog_items[self.player][self.item] >= self.count
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
return self, self(state)
diff --git a/worlds/stardew_valley/test/TestStardewRule.py b/worlds/stardew_valley/test/TestStardewRule.py
index 93b32b0d8ab4..842e4589c65f 100644
--- a/worlds/stardew_valley/test/TestStardewRule.py
+++ b/worlds/stardew_valley/test/TestStardewRule.py
@@ -1,4 +1,5 @@
import unittest
+from collections import Counter
from typing import cast
from unittest.mock import MagicMock, Mock
@@ -103,7 +104,7 @@ def test_short_circuit_when_complement_found(self):
def test_short_circuit_when_combinable_rules_is_false(self):
collection_state = MagicMock()
- collection_state.has = Mock(return_value=False)
+ collection_state.prog_items = {1: Counter()}
other_rule = MagicMock()
rule = And(Received("Potato", 1, 10), cast(StardewRule, other_rule))
@@ -113,6 +114,7 @@ def test_short_circuit_when_combinable_rules_is_false(self):
def test_identity_is_removed_from_other_rules(self):
collection_state = MagicMock()
+ collection_state.prog_items = {1: Counter()}
rule = Or(false_, Received("Potato", 1, 10))
rule.evaluate_while_simplifying(collection_state)
@@ -122,6 +124,7 @@ def test_identity_is_removed_from_other_rules(self):
def test_complement_replaces_combinable_rules(self):
collection_state = MagicMock()
+ collection_state.prog_items = {1: Counter()}
rule = Or(Received("Potato", 1, 10), true_)
rule.evaluate_while_simplifying(collection_state)
@@ -132,6 +135,7 @@ def test_simplifying_to_complement_propagates_complement(self):
expected_simplified = true_
expected_result = True
collection_state = MagicMock()
+ collection_state.prog_items = {1: Counter()}
rule = Or(Or(expected_simplified), Received("Potato", 1, 10))
actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state)
@@ -142,6 +146,7 @@ def test_simplifying_to_complement_propagates_complement(self):
def test_already_simplified_rules_are_not_simplified_again(self):
collection_state = MagicMock()
+ collection_state.prog_items = {1: Counter()}
other_rule = MagicMock()
other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, False))
rule = Or(cast(StardewRule, other_rule), Received("Potato", 1, 10))
diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv
index 6bc1183e598d..9fded3719817 100644
--- a/worlds/terraria/Rules.dsv
+++ b/worlds/terraria/Rules.dsv
@@ -382,6 +382,9 @@ Hardmode Forge; ;
Adamantite Bar; ; (Hardmode Forge & Adamantite Ore) | Wall of Flesh;
Adamantite Pickaxe; Pickaxe(180); Hardmode Anvil & Adamantite Bar;
Forbidden Armor; Armor Minions(2); Hardmode Anvil & Adamantite Bar & Forbidden Fragment;
+Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental;
+Unholy Core; Calamity; (Infernal Suevite & Hellstone) | Brimstone Elemental;
+Ruin Medallion; Calamity; (Hardmode Anvil & Coin of Deceit & Unholy Core & Essence of Havoc) | Dark Matter Sheath;
Aquatic Scourge; Calamity | Location | Item;
Cragmaw Mire; Calamity | Location | Item; #Acid Rain Tier 2;
Nuclear Fuel Rod; Calamity | Minions(1); #Cragmaw Mire | Star-Tainted Generator;
@@ -411,9 +414,6 @@ Topped Off; Achievement;
Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3);
// Brimstone Elemental
-Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental;
-Unholy Core; Calamity; (Infernal Suevite & Hellstone) | Brimstone Elemental;
-Ruin Medallion; Calamity; (Hardmode Anvil & Coin of Deceit & Unholy Core & Essence of Havoc) | Dark Matter Sheath;
// The Destroyer
Soul of Might; ; #The Destroyer | Avenger Emblem | Light Disc | (@calamity & (Mechanical Glove | Celestial Emblem));
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index 3d80fd245875..19b1b800ec64 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -2,10 +2,10 @@
Archipelago init file for The Witness
"""
import dataclasses
-from logging import error, warning
+from logging import error, info, warning
from typing import Any, Dict, List, Optional, cast
-from BaseClasses import CollectionState, Entrance, Location, LocationProgressType, Region, Tutorial
+from BaseClasses import CollectionState, Entrance, Item, Location, LocationProgressType, Region, Tutorial
from Options import OptionError, PerGameCommonOptions, Toggle
from worlds.AutoWorld import WebWorld, World
@@ -18,6 +18,7 @@
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations
from .options import TheWitnessOptions, witness_option_groups
+from .place_early_item import place_early_items
from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
from .presets import witness_option_presets
@@ -81,7 +82,7 @@ class WitnessWorld(World):
item_name_groups = static_witness_items.ITEM_GROUPS
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
- required_client_version = (0, 6, 4)
+ required_client_version = (0, 6, 8)
player_logic: WitnessPlayerLogic
player_locations: WitnessPlayerLocations
@@ -94,6 +95,7 @@ class WitnessWorld(World):
items_placed_early: List[str]
own_itempool: List[WitnessItem]
+ reachable_early_locations: List[str]
panel_hunt_required_count: int
def _get_slot_data(self) -> Dict[str, Any]:
@@ -108,7 +110,7 @@ def _get_slot_data(self) -> Dict[str, Any]:
"hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES],
"log_ids_to_hints": self.log_ids_to_hints,
"laser_ids_to_hints": self.laser_ids_to_hints,
- "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
+ "progressive_item_lists": self.player_items.get_progressive_item_ids(),
"obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES],
"panel_hunt_required_absolute": self.panel_hunt_required_count
@@ -218,25 +220,6 @@ def create_regions(self) -> None:
self.own_itempool.append(dog_puzzle_skip)
self.items_placed_early.append("Puzzle Skip")
- if self.options.early_symbol_item:
- # Pick an early item to place on the tutorial gate.
- early_items = [
- item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
- ]
- if early_items:
- random_early_item = self.random.choice(early_items)
- mode = self.options.puzzle_randomization
- if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
- # In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
- # Same with panel hunt, since the Tutorial Gate Open panel is used for something else
- self.multiworld.local_early_items[self.player][random_early_item] = 1
- else:
- # Force the item onto the tutorial gate check and remove it from our random pool.
- gate_item = self.create_item(random_early_item)
- self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
- self.own_itempool.append(gate_item)
- self.items_placed_early.append(random_early_item)
-
# There are some really restrictive options in The Witness.
# They are rarely played, but when they are, we add some extra sphere 1 locations.
# This is done both to prevent generation failures, but also to make the early game less linear.
@@ -245,20 +228,39 @@ def create_regions(self) -> None:
state = CollectionState(self.multiworld)
state.sweep_for_advancements(locations=event_locations)
- num_early_locs = sum(
- 1 for loc in self.multiworld.get_reachable_locations(state, self.player)
- if loc.address and not loc.item
+ # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
+
+ early_locations = [
+ location for location in self.multiworld.get_reachable_locations(state, self.player)
+ if not location.is_event and not location.item
+ ]
+ self.reachable_early_locations = [location.name for location in early_locations]
+
+ num_reachable_tutorial_locations = sum(
+ static_witness_logic.ALL_REGIONS_BY_NAME[
+ cast(Region, location.parent_region).name
+ ].area.name == "Tutorial (Inside)"
+ for location in early_locations
)
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
- needed_size = 2
- needed_size += self.options.puzzle_randomization == "sigma_expert"
- needed_size += self.options.shuffle_symbols
- needed_size += self.options.shuffle_doors != "off"
+ needed_size_overall = 2
+ needed_size_overall += self.options.puzzle_randomization == "sigma_expert"
+ needed_size_overall += self.options.shuffle_symbols
+ needed_size_overall += self.options.shuffle_doors != "off"
+
+ needed_size_to_hold_tutorial_items = len(
+ self.player_items.get_early_items(set(self.player_items.get_mandatory_items()))
+ )
# Then, add checks in order until the required amount of sphere 1 checks is met.
+ extra_tutorial_checks = [
+ ("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
+ ("Tutorial First Hallway", "Tutorial First Hallway Straight")
+ ]
+
extra_checks = [
("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
("Tutorial First Hallway", "Tutorial First Hallway Straight"),
@@ -266,13 +268,30 @@ def create_regions(self) -> None:
("Desert Outside", "Desert Surface 2"),
]
- for i in range(num_early_locs, needed_size):
+ for _ in range(num_reachable_tutorial_locations, needed_size_to_hold_tutorial_items):
+ if not extra_tutorial_checks:
+ break
+
+ region, loc = extra_tutorial_checks.pop(0)
+ extra_checks.pop(0)
+ self.player_locations.add_location_late(loc)
+ self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation)
+ self.reachable_early_locations.append(loc)
+
+ player = self.player_name
+
+ info(
+ f"""Location "{loc}" had to be added to {player}'s world to hold the requested early good items."""
+ )
+
+ for _ in range(len(self.reachable_early_locations), needed_size_overall):
if not extra_checks:
break
region, loc = extra_checks.pop(0)
self.player_locations.add_location_late(loc)
- self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
+ self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation)
+ self.reachable_early_locations.append(loc)
warning(
f"""Location "{loc}" had to be added to {self.player_name}'s world
@@ -341,6 +360,10 @@ def create_items(self) -> None:
self.own_itempool += new_items
self.multiworld.itempool += new_items
+ def fill_hook(self, progitempool: List[Item], _: List[Item], _2: List[Item],
+ fill_locations: List[Location]) -> None:
+ place_early_items(self, progitempool, fill_locations)
+
def fill_slot_data(self) -> Dict[str, Any]:
already_hinted_locations = set()
@@ -406,19 +429,47 @@ def create_item(self, item_name: str) -> WitnessItem:
else:
item_data = static_witness_items.ITEM_DATA[item_name]
- return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
+ item = WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
+
+ item.is_alias_for = static_witness_items.ALL_ITEM_ALIASES.get(item_name, None)
+ if hasattr(self, "player_items") and self.player_items:
+ item.progressive_chain = self.player_items.all_progressive_item_lists.get(item_name, None)
+
+ return item
+
+ def collect(self, state: CollectionState, item: WitnessItem) -> bool:
+ if not super().collect(state, item):
+ return False
- def collect(self, state: "CollectionState", item: WitnessItem) -> bool:
- changed = super().collect(state, item)
- if changed and item.eggs:
+ if item.eggs:
state.prog_items[self.player]["Egg"] += item.eggs
- return changed
- def remove(self, state: "CollectionState", item: WitnessItem) -> bool:
- changed = super().remove(state, item)
- if changed and item.eggs:
+ elif item.is_alias_for:
+ state.prog_items[self.player][item.is_alias_for] += 1
+
+ elif item.progressive_chain:
+ index = state.prog_items[self.player][item.name] - 1
+ if index < len(item.progressive_chain):
+ state.prog_items[self.player][item.progressive_chain[index]] += 1
+
+ return True
+
+ def remove(self, state: CollectionState, item: WitnessItem) -> bool:
+ if not super().remove(state, item):
+ return False
+
+ if item.eggs:
state.prog_items[self.player]["Egg"] -= item.eggs
- return changed
+
+ elif item.is_alias_for:
+ state.prog_items[self.player][item.is_alias_for] -= 1
+
+ elif item.progressive_chain:
+ index = state.prog_items[self.player][item.name]
+ if index < len(item.progressive_chain):
+ state.prog_items[self.player][item.progressive_chain[index]] -= 1
+
+ return True
def get_filler_item_name(self) -> str:
return "Speed Boost"
diff --git a/worlds/witness/data/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt
index 57aee28e45b6..4ebbc49669e4 100644
--- a/worlds/witness/data/WitnessItems.txt
+++ b/worlds/witness/data/WitnessItems.txt
@@ -2,8 +2,8 @@ Symbols:
0 - Dots
1 - Colored Dots
2 - Full Dots
-3 - Invisible Dots
5 - Sound Dots
+7 - Sparse Dots
10 - Symmetry
20 - Triangles
30 - Eraser
@@ -12,12 +12,16 @@ Symbols:
50 - Negative Shapers
60 - Stars
61 - Stars + Same Colored Symbol
+67 - Simple Stars
71 - Black/White Squares
72 - Colored Squares
80 - Arrows
-200 - Progressive Dots - Dots,Full Dots
-210 - Progressive Symmetry - Symmetry,Colored Dots
-260 - Progressive Stars - Stars,Stars + Same Colored Symbol
+200 - Progressive Dots
+240 - Progressive Shapers
+210 - Progressive Symmetry
+260 - Progressive Stars
+270 - Progressive Squares
+280 - Progressive Discard Symbols
Useful:
510 - Puzzle Skip
diff --git a/worlds/witness/data/item_definition_classes.py b/worlds/witness/data/item_definition_classes.py
index b095a83abe63..3eb77d7509b0 100644
--- a/worlds/witness/data/item_definition_classes.py
+++ b/worlds/witness/data/item_definition_classes.py
@@ -35,7 +35,7 @@ class ItemDefinition:
@dataclass(frozen=True)
class ProgressiveItemDefinition(ItemDefinition):
- child_item_names: List[str]
+ pass
@dataclass(frozen=True)
diff --git a/worlds/witness/data/settings/Symbol_Shuffle.txt b/worlds/witness/data/settings/Symbol_Shuffle.txt
index 253fe98bad42..604d1c106405 100644
--- a/worlds/witness/data/settings/Symbol_Shuffle.txt
+++ b/worlds/witness/data/settings/Symbol_Shuffle.txt
@@ -1,13 +1,18 @@
Items:
Arrows
-Progressive Dots
+Dots
+Sparse Dots
+Full Dots
Sound Dots
-Progressive Symmetry
+Symmetry
+Colored Dots
Triangles
Eraser
Shapers
Rotated Shapers
Negative Shapers
-Progressive Stars
+Stars
+Simple Stars
+Stars + Same Colored Symbol
Black/White Squares
Colored Squares
\ No newline at end of file
diff --git a/worlds/witness/data/settings/progressive_items.py b/worlds/witness/data/settings/progressive_items.py
new file mode 100644
index 000000000000..869f0b30fcf5
--- /dev/null
+++ b/worlds/witness/data/settings/progressive_items.py
@@ -0,0 +1,8 @@
+PROGRESSIVE_SYMBOLS = {
+ "Progressive Dots": ["Dots", "Full Dots"],
+ "Progressive Symmetry": ["Symmetry", "Colored Dots"],
+ "Progressive Stars": ["Stars", "Stars + Same Colored Symbol"],
+ "Progressive Shapers": ["Shapers", "Rotated Shapers", "Negative Shapers"],
+ "Progressive Squares": ["Black/White Squares", "Colored Squares"],
+ "Progressive Discard Symbols": ["Triangles", "Arrows"],
+}
diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py
index c64df741982e..afd592c3bdb2 100644
--- a/worlds/witness/data/static_items.py
+++ b/worlds/witness/data/static_items.py
@@ -13,7 +13,15 @@
# item list during get_progression_items.
_special_usefuls: List[str] = ["Puzzle Skip"]
-ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
+
+ALL_ITEM_ALIASES: Dict[str, str] = { # Keeping this as str->str for now for efficiency
+ "Sparse Dots": "Dots",
+ "Simple Stars": "Stars",
+}
+
+ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {
+ "Dots", "Sparse Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars", "Simple Stars"
+}
MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = {
"none": set(),
diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py
index bfe92467fb61..4445f566726a 100644
--- a/worlds/witness/data/static_logic.py
+++ b/worlds/witness/data/static_logic.py
@@ -294,7 +294,6 @@ def combine_connections(self) -> None:
# Item data parsed from WitnessItems.txt
ALL_ITEMS: Dict[str, ItemDefinition] = {}
-_progressive_lookup: Dict[str, str] = {}
def parse_items() -> None:
@@ -328,22 +327,13 @@ def parse_items() -> None:
# Read filler weights.
weight = int(arguments[0]) if len(arguments) >= 1 else 1
ALL_ITEMS[item_name] = WeightedItemDefinition(item_code, current_category, weight)
- elif arguments:
+ elif item_name.startswith("Progressive"):
# Progressive items.
- ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category, arguments)
- for child_item in arguments:
- _progressive_lookup[child_item] = item_name
+ ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category)
else:
ALL_ITEMS[item_name] = ItemDefinition(item_code, current_category)
-def get_parent_progressive_item(item_name: str) -> str:
- """
- Returns the name of the item's progressive parent, if there is one, or the item's name if not.
- """
- return _progressive_lookup.get(item_name, item_name)
-
-
@cache_argsless
def get_vanilla() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_vanilla_logic())
diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py
index f04c1f6d3738..7f5469a3d56c 100644
--- a/worlds/witness/hints.py
+++ b/worlds/witness/hints.py
@@ -46,6 +46,8 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
"Boat",
"Caves Shortcuts",
"Progressive Dots",
+ "Dots",
+ "Sparse Dots",
]
difficulty = world.options.puzzle_randomization
@@ -100,7 +102,11 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
if world.options.shuffle_symbols:
symbols = [
"Progressive Dots",
+ "Dots",
+ "Sparse Dots",
"Progressive Stars",
+ "Stars",
+ "Simple Stars",
"Shapers",
"Rotated Shapers",
"Negative Shapers",
@@ -110,9 +116,15 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
"Black/White Squares",
"Colored Squares",
"Sound Dots",
- "Progressive Symmetry"
+ "Progressive Symmetry",
+ "Progressive Shapers",
+ "Progressive Squares",
+ "Progressive Discard Symbols",
]
+ # Only consider symbols that are actually in the pool
+ symbols = [symbol for symbol in symbols if symbol in world.player_items.item_data]
+
priority.update(world.random.sample(symbols, 5))
if world.options.shuffle_lasers:
diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py
index e7f6f94d659e..f4416b72ce12 100644
--- a/worlds/witness/locations.py
+++ b/worlds/witness/locations.py
@@ -72,5 +72,5 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N
def add_location_late(self, entity_name: str) -> None:
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
- self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex)
+ self.CHECK_LOCATION_TABLE[entity_name] = static_witness_locations.get_id(entity_hex)
self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)
diff --git a/worlds/witness/options.py b/worlds/witness/options.py
index 6a64fdb3d877..2fcd35b64c56 100644
--- a/worlds/witness/options.py
+++ b/worlds/witness/options.py
@@ -1,8 +1,6 @@
from dataclasses import dataclass
from typing import Tuple
-from schema import And, Schema
-
from Options import (
Choice,
DefaultOnToggle,
@@ -13,6 +11,7 @@
OptionSet,
PerGameCommonOptions,
Range,
+ Removed,
Toggle,
Visibility,
)
@@ -50,12 +49,19 @@ class EarlyCaves(Choice):
alias_on = 2
-class EarlySymbolItem(DefaultOnToggle):
+class EarlyGoodItems(OptionSet):
"""
- Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early.
+ Put one random helpful item of each of the chosen types on an early check, specifically a sphere 1 Tutorial location.
+ If a type is chosen, but no items of that type exist in the itempool, it is skipped.
+ The possible types are: "Symbol", "Door / Door Panel", "Obelisk Key".
+
+ If there aren't enough sphere 1 Tutorial locations, Tutorial First Hallway Straight and Tutorial First Hallway Bend may be added as locations.
+ If there still aren't enough sphere 1 Tutorial locations, a random local sphere 1 location is picked.
+ If no local sphere 1 locations are available, there are no further attempts to place the item.
"""
- visibility = Visibility.none
+ valid_keys = {"Symbol", "Door / Door Panel", "Obelisk Key"}
+ default = frozenset({"Symbol"})
class ShuffleSymbols(DefaultOnToggle):
@@ -68,6 +74,72 @@ class ShuffleSymbols(DefaultOnToggle):
display_name = "Shuffle Symbols"
+class ProgressiveSymbols(OptionSet):
+ """
+ Make some symbols progressive, if they exist.
+
+ By default, includes the chains where the second item can't be used without the first.
+
+ Progressive Dots: Dots -> Full Dots
+ Progressive Symmetry: Symmetry -> Colored Dots
+ Progressive Stars: Stars -> Stars + Same Colored Symbol
+ Progressive Squares: Black/White Squares -> Colored Squares
+ Progressive Shapers: Shapers -> Rotated Shapers -> Negative Shapers
+ Progressive Discard Symbols: Triangles -> Arrows
+ """
+ display_name = "Progressive Symbols"
+
+ valid_keys = {
+ "Progressive Dots",
+ "Progressive Symmetry",
+ "Progressive Stars",
+ "Progressive Squares",
+ "Progressive Shapers",
+ "Progressive Discard Symbols"
+ }
+
+ default = frozenset({"Progressive Dots", "Progressive Symmetry", "Progressive Stars"})
+
+
+class SecondStageSymbolsActIndependently(OptionSet):
+ """
+ Makes certain second stage symbols act independently of first stage symbols if they are not progressive.
+
+ - "Full Dots": "Full Dots" unlocks Full Dots panels even if you don't have "Dots". "Dots" is renamed to "Sparse Dots".
+ - "Stars + Same Colored Symbol": "Stars + Same Colored Symbol" unlocks Stars + Same Colored Symbol panels even if you don't have "Stars". "Stars" is renamed to "Simlpe Stars".
+ - "Colored Dots": Removes the Symmetry requirement from the Symmetry Laser panel sets so that Colored Dots can unlock something on their own. This is on by default.
+
+ Rotated Shapers always act independently from Shapers. The ability to make them dependent on Shapers by omitting them in this option may be added in the future.
+ """
+
+ valid_keys = {
+ "Full Dots",
+ "Stars + Same Colored Symbol",
+ "Colored Dots",
+ }
+
+ default = frozenset({"Colored Dots"})
+
+ visibility = Visibility.template | Visibility.complex_ui
+
+
+class ColoredDotsAreProgressiveDots(Toggle):
+ """
+ Put Colored Dots into the "Progressive Dots" group, after Dots.
+ This removes Progressive Symmetry.
+ """
+
+ visibility = Visibility.template | Visibility.complex_ui
+
+
+class SoundDotsAreProgressiveDots(Toggle):
+ """
+ Put Sound Dots into the "Progressive Dots" group, before Full Dots.
+ """
+
+ visibility = Visibility.template | Visibility.complex_ui
+
+
class ShuffleLasers(Choice):
"""
If on, the 11 lasers are turned into items and will activate on their own upon receiving them.
@@ -529,6 +601,10 @@ class PuzzleRandomizationSeed(Range):
class TheWitnessOptions(PerGameCommonOptions):
puzzle_randomization: PuzzleRandomization
shuffle_symbols: ShuffleSymbols
+ progressive_symbols: ProgressiveSymbols
+ colored_dots_are_progressive_dots: ColoredDotsAreProgressiveDots
+ sound_dots_are_progressive_dots: SoundDotsAreProgressiveDots
+ second_stage_symbols_act_independently: SecondStageSymbolsActIndependently
shuffle_doors: ShuffleDoors
door_groupings: DoorGroupings
shuffle_boat: ShuffleBoat
@@ -550,7 +626,7 @@ class TheWitnessOptions(PerGameCommonOptions):
panel_hunt_discourage_same_area_factor: PanelHuntDiscourageSameAreaFactor
panel_hunt_plando: PanelHuntPlando
early_caves: EarlyCaves
- early_symbol_item: EarlySymbolItem
+ early_good_items: EarlyGoodItems
elevators_come_to_you: ElevatorsComeToYou
trap_percentage: TrapPercentage
trap_weights: TrapWeights
@@ -565,6 +641,8 @@ class TheWitnessOptions(PerGameCommonOptions):
shuffle_dog: ShuffleDog
easter_egg_hunt: EasterEggHunt
+ early_symbol_item: Removed
+
witness_option_groups = [
OptionGroup("Puzzles & Goal", [
@@ -590,6 +668,10 @@ class TheWitnessOptions(PerGameCommonOptions):
]),
OptionGroup("Progression Items", [
ShuffleSymbols,
+ ProgressiveSymbols,
+ SecondStageSymbolsActIndependently,
+ ColoredDotsAreProgressiveDots,
+ SoundDotsAreProgressiveDots,
ShuffleDoors,
DoorGroupings,
ShuffleLasers,
@@ -611,6 +693,7 @@ class TheWitnessOptions(PerGameCommonOptions):
LaserHints
]),
OptionGroup("Misc", [
+ EarlyGoodItems,
EarlyCaves,
ElevatorsComeToYou,
DeathLink,
diff --git a/worlds/witness/place_early_item.py b/worlds/witness/place_early_item.py
new file mode 100644
index 000000000000..0490dbebf194
--- /dev/null
+++ b/worlds/witness/place_early_item.py
@@ -0,0 +1,151 @@
+from logging import debug, error
+from typing import TYPE_CHECKING, Dict, List, Set, Tuple
+
+from BaseClasses import CollectionState, Item, Location, LocationProgressType
+
+from .data import static_logic as static_witness_logic
+from .data.utils import cast_not_none
+
+if TYPE_CHECKING:
+ from . import WitnessWorld
+
+
+def get_available_early_locations(world: "WitnessWorld") -> List[Location]:
+ # Pick an early item to put on Tutorial Gate Open.
+ # Done after plando to avoid conflicting with it.
+ # Done in fill_hook because multiworld itempool manipulation is not allowed in pre_fill.
+
+ # Prioritize Tutorial locations in a specific order
+ tutorial_checks_in_order = [
+ "Tutorial Gate Open",
+ "Tutorial Back Left",
+ "Tutorial Back Right",
+ "Tutorial Front Left",
+ "Tutorial First Hallway Straight",
+ "Tutorial First Hallway Bend",
+ "Tutorial Patio Floor",
+ "Tutorial First Hallway EP",
+ "Tutorial Cloud EP",
+ "Tutorial Patio Flowers EP",
+ ]
+ available_locations = [
+ world.get_location(location_name) for location_name in tutorial_checks_in_order
+ if location_name in world.reachable_early_locations # May not actually be sphere 1 (e.g. Obelisk Keys for EPs)
+ ]
+
+ # Then, add the rest of sphere 1 in "game order"
+ available_locations += sorted(
+ (
+ world.get_location(location_name) for location_name in world.reachable_early_locations
+ if location_name not in tutorial_checks_in_order
+ ),
+ key=lambda location_object: static_witness_logic.ENTITIES_BY_NAME[location_object.name]["order"]
+ )
+
+ return [
+ location for location in available_locations
+ if not location.item and location.progress_type != LocationProgressType.EXCLUDED
+ ]
+
+
+def get_eligible_items_by_type_in_random_order(world: "WitnessWorld") -> Dict[str, List[str]]:
+ eligible_early_items_by_type = world.player_items.get_early_items({item.name for item in world.own_itempool})
+
+ for item_list in eligible_early_items_by_type.values():
+ world.random.shuffle(item_list)
+
+ return eligible_early_items_by_type
+
+
+def grab_own_items_from_itempool(world: "WitnessWorld", itempool: List[Item], ids_to_find: Set[int]) -> List[Item]:
+ found_early_items = []
+
+ def keep_or_take_out(item: Item) -> bool:
+ if item.code not in ids_to_find:
+ return True # Keep
+ ids_to_find.remove(item.code)
+ found_early_items.append(item)
+ return False # Take out
+
+ local_player = world.player
+ itempool[:] = [item for item in itempool if item.player != local_player or keep_or_take_out(item)]
+
+ return found_early_items
+
+
+def place_items_onto_locations(world: "WitnessWorld", items: List[Item],
+ locations: List[Location]) -> Tuple[List[Item], List[Item]]:
+ fake_state = CollectionState(world.multiworld)
+
+ placed_items = []
+ unplaced_items = []
+
+ for item in items:
+ location = next(
+ (location for location in locations if location.can_fill(fake_state, item, check_access=False)),
+ None,
+ )
+ if location is not None:
+ location.place_locked_item(item)
+ placed_items.append(item)
+ locations.remove(location)
+ else:
+ unplaced_items.append(item)
+
+ return placed_items, unplaced_items
+
+
+def place_early_items(world: "WitnessWorld", prog_itempool: List[Item], fill_locations: List[Location]) -> None:
+ if not world.options.early_good_items.value:
+ return
+
+ # Get a list of good early locations in a determinstic order
+ eligible_early_locations = get_available_early_locations(world)
+ # Get a list of good early items of each desired item type
+ eligible_early_items_by_type = get_eligible_items_by_type_in_random_order(world)
+
+ if not eligible_early_items_by_type:
+ return
+
+ while any(eligible_early_items_by_type.values()) and eligible_early_locations:
+ # Get one item of each type
+ next_findable_items_dict = {
+ item_list.pop(): item_type
+ for item_type, item_list in eligible_early_items_by_type.items()
+ if item_list
+ }
+
+ # Get their IDs as a set
+ next_findable_item_ids = {world.item_name_to_id[item_name] for item_name in next_findable_items_dict}
+
+ # Grab items from itempool
+ found_early_items = grab_own_items_from_itempool(world, prog_itempool, next_findable_item_ids)
+
+ # Bring found items back into Symbol -> Door -> Obelisk Key order
+ # The intent is that the Symbol is always on Tutorial Gate Open / generally that the order is predictable
+ correct_order = {item_name: i for i, item_name in enumerate(next_findable_items_dict)}
+ found_early_items.sort(key=lambda item: correct_order[item.name])
+
+ # Place found early items on eligible early locations.
+ placed_items, unplaced_items = place_items_onto_locations(world, found_early_items, eligible_early_locations)
+
+ for item in placed_items:
+ debug(f"Placed early good item {item} on early location {item.location}.")
+ # Item type is satisfied
+ del eligible_early_items_by_type[next_findable_items_dict[item.name]]
+ fill_locations.remove(cast_not_none(item.location))
+ for item in unplaced_items:
+ debug(f"Could not find a suitable placement for item {item}.")
+
+ unfilled_types = list(eligible_early_items_by_type)
+ if unfilled_types:
+ if not eligible_early_locations:
+ error(
+ f'Could not find a suitable location for "early good items" of types {unfilled_types} in '
+ f"{world.player_name}'s world. They are excluded or already contain plandoed items.\n"
+ )
+ else:
+ error(
+ f"Could not find any \"early good item\" of types {unfilled_types} in {world.player_name}'s world, "
+ "they were all plandoed elsewhere."
+ )
diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py
index d13ebcafdcaf..dd2856a72797 100644
--- a/worlds/witness/player_items.py
+++ b/worlds/witness/player_items.py
@@ -7,7 +7,6 @@
from BaseClasses import Item, ItemClassification, MultiWorld
from .data import static_items as static_witness_items
-from .data import static_logic as static_witness_logic
from .data.item_definition_classes import (
DoorItemDefinition,
ItemCategory,
@@ -32,6 +31,8 @@ class WitnessItem(Item):
"""
game: str = "The Witness"
eggs: int = 0
+ is_alias_for: str | None = None
+ progressive_chain: list[str] | None = None
@classmethod
def make_egg_event(cls, item_name: str, player: int):
@@ -55,12 +56,31 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
self._logic: WitnessPlayerLogic = player_logic
self._locations: WitnessPlayerLocations = player_locations
+ self.replacement_items = {}
+ # Make item aliases for "Sparse Dots" and "Simple Stars" if necessary
+ if "Full Dots" in world.options.second_stage_symbols_act_independently:
+ self.replacement_items["Dots"] = "Sparse Dots"
+ if "Stars + Same Colored Symbol" in world.options.second_stage_symbols_act_independently:
+ self.replacement_items["Stars"] = "Simple Stars"
+
+ assert all(
+ static_witness_items.ALL_ITEM_ALIASES.get(value, None) == key
+ for key, value in self.replacement_items.items()
+ ), "A replacement item was used without setting up the alias in static_witness_items.ALL_ITEM_ALIASES"
+
+ self.all_progressive_item_lists = copy.deepcopy(self._logic.THEORETICAL_PROGRESSIVE_LISTS)
+ self.progressive_item_lists_in_use = copy.deepcopy(self._logic.FINALIZED_PROGRESSIVE_LISTS)
+
+ self.progressive_item_lookup: Dict[str, str] = {}
+ for progressive_item, chain_items in self.progressive_item_lists_in_use.items():
+ self.progressive_item_lookup.update({chain_item: progressive_item for chain_item in chain_items})
+
# Duplicate the static item data, then make any player-specific adjustments to classification.
self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA)
# Remove all progression items that aren't actually in the game.
self.item_data = {
- name: data for (name, data) in self.item_data.items()
+ self.replacement_items.get(name, name): data for (name, data) in self.item_data.items()
if ItemClassification.progression not in data.classification
or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME
}
@@ -86,7 +106,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
}
for item_name, item_data in progression_dict.items():
if isinstance(item_data.definition, ProgressiveItemDefinition):
- num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name])
+ num_progression = len(self.progressive_item_lists_in_use[item_name])
self._mandatory_items[item_name] = num_progression
else:
self._mandatory_items[item_name] = 1
@@ -141,9 +161,15 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
if self._world.options.puzzle_randomization == "umbra_variety":
self._proguseful_items.add("Triangles")
- # This needs to be improved when the improved independent&progressive symbols PR is merged
- for item in list(self._proguseful_items):
- self._proguseful_items.add(static_witness_logic.get_parent_progressive_item(item))
+ for progressive_item, progressive_item_chain in player_logic.FINALIZED_PROGRESSIVE_LISTS.items():
+ for chain_item in progressive_item_chain:
+ if chain_item in self._proguseful_items:
+ self._proguseful_items.add(progressive_item)
+ break
+
+ for alias_item, real_item in static_witness_items.ALL_ITEM_ALIASES.items():
+ if real_item in self._proguseful_items:
+ self._proguseful_items.add(alias_item)
for item_name, item_data in self.item_data.items():
if item_name in self._proguseful_items:
@@ -198,36 +224,118 @@ def get_filler_items(self, quantity: int) -> Dict[str, int]:
return output
- def get_early_items(self) -> List[str]:
+ def get_early_items(self, existing_items: Set[str]) -> Dict[str, List[str]]:
"""
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
"""
- output: Set[str] = set()
- if self._world.options.shuffle_symbols:
- discards_on = self._world.options.shuffle_discarded_panels
- mode = self._world.options.puzzle_randomization.current_key
-
- output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
- if discards_on:
- output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
-
- # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
- # before create_items so that we'll be able to check placed items instead of just removing all items mentioned
- # regardless of whether or not they actually wind up being manually placed.
- for plando_setting in self._world.options.plando_items:
- if plando_setting.from_pool:
- if isinstance(plando_setting.items, dict):
- output -= {item for item, weight in plando_setting.items.items() if weight}
+ output: Dict[str, List[str]] = {}
+
+ if "Symbol" in self._world.options.early_good_items.value:
+ good_symbols = ["Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"]
+
+ if self._world.options.shuffle_discarded_panels:
+ if self._world.options.puzzle_randomization == "sigma_expert":
+ good_symbols.append("Arrows")
else:
- # Assume this is some other kind of iterable.
- for inner_item in plando_setting.items:
- if isinstance(inner_item, str):
- output -= {inner_item}
- elif isinstance(inner_item, dict):
- output -= {item for item, weight in inner_item.items() if weight}
+ good_symbols.append("Triangles")
+
+ # Replace progressive items with their parents.
+ good_symbols = [
+ self.progressive_item_lookup.get(item, item) for item in good_symbols
+ ]
+
+ output["Symbol"] = [symbol for symbol in good_symbols if symbol in existing_items]
+
+ if "Door / Door Panel" in self._world.options.early_good_items.value:
+ good_doors = [
+ "Desert Doors & Elevator", "Keep Hedge Maze Doors", "Keep Pressure Plates Doors",
+ "Shadows Lower Doors", "Tunnels Doors", "Quarry Stoneworks Doors",
+
+ "Keep Tower Shortcut (Door)", "Shadows Timed Door", "Tunnels Town Shortcut (Door)",
+ "Quarry Stoneworks Roof Exit (Door)",
+
+ "Desert Panels", "Keep Hedge Maze Panels",
+
+ "Shadows Door Timer (Panel)",
+ ]
+
+ # While Caves Shortcuts don't unlock anything in symbol shuffle, you'd still rather have them early.
+ # But, we need to do some special handling for them.
+
+ if self._world.options.shuffle_doors in ("doors", "mixed"):
+ # Caves Shortcuts might exist in vanilla/panel doors because of "early caves: add to pool".
+ # But if the player wanted them early, they would have chosen "early caves: starting inventory".
+ # This is why we make sure these are "natural" Caves Shortcuts.
+ good_doors.append("Caves Shortcuts")
+
+ # These two doors are logically equivalent, so we choose a random one as to not give them twice the chance.
+ good_doors.append(
+ self._world.random.choice(["Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)"])
+ )
+
+ if self._world.options.shuffle_vault_boxes and not self._world.options.disable_non_randomized_puzzles:
+ good_doors.append("Windmill & Theater Doors")
+ if not self._world.options.shuffle_symbols:
+ good_doors += [
+ "Windmill & Theater Panels",
- # Sort the output for consistency across versions if the implementation changes but the logic does not.
- return sorted(output)
+ "Windmill & Theater Control Panels",
+ ]
+
+ if self._world.options.shuffle_doors == "doors": # It's not as good in mixed doors because of Light Control
+ good_doors.append("Desert Light Room Entry (Door)")
+
+ if not self._world.options.shuffle_symbols:
+ good_doors += [
+ "Bunker Doors", "Swamp Doors", "Glass Factory Doors", "Town Doors",
+
+ "Bunker Entry (Door)", "Glass Factory Entry (Door)", "Symmetry Island Lower (Door)",
+
+ "Bunker Panels", "Swamp Panels", "Quarry Outside Panels", "Glass Factory Panels"
+
+ "Glass Factory Entry (Panel)",
+ ]
+
+ existing_doors = [door for door in good_doors if door in existing_items]
+
+ # On some options combinations with doors, there just aren't a lot of doors that unlock much early.
+ # In this case, we add some doors that aren't great, but are at least guaranteed to unlock 1 location.
+
+ fallback_doors = [
+ "Keep Shadows Shortcut (Door)", # Always Keep Shadows Shortcut Panel
+ "Keep Shortcuts", # -"-
+ "Keep Pressure Plates Doors", # -"-
+ "Keep Hedge Maze 1 (Panel)", # Always Hedge 1
+ "Town Maze Stairs (Panel)", # Always Maze Panel
+ "Shadows Laser Room Doors", # Always Shadows Laser Panel
+ "Swamp Laser Shortcut (Door)", # Always Swamp Laser
+ "Town Maze Panels", # Always Town Maze Panel
+ "Town Doors", # Always Town Church Lattice
+ "Town Church Entry (Door)", # -"-
+ "Town Tower Doors", # Always Town Laser
+ ]
+ self._world.random.shuffle(fallback_doors)
+
+ while len(existing_doors) < 4 and fallback_doors:
+ fallback_door = fallback_doors.pop()
+ if fallback_door in existing_items and fallback_door not in existing_doors:
+ existing_doors.append(fallback_door)
+
+ output["Door"] = existing_doors
+
+ if "Obelisk Key" in self._world.options.early_good_items.value:
+ obelisk_keys = [
+ "Desert Obelisk Key", "Town Obelisk Key", "Quarry Obelisk Key",
+ "Treehouse Obelisk Key", "Monastery Obelisk Key", "Mountainside Obelisk Key"
+ ]
+ output["Obelisk Key"] = [key for key in obelisk_keys if key in existing_items]
+
+ assert all(item in self._world.item_names for sublist in output.values() for item in sublist), (
+ [item for sublist in output.values() for item in sublist if item not in self._world.item_names]
+ )
+
+ # Cull empty lists
+ return {item_type: item_list for item_type, item_list in output.items() if item_list}
def get_door_item_ids_in_pool(self) -> List[int]:
"""
@@ -249,13 +357,14 @@ def get_symbol_ids_not_in_pool(self) -> List[int]:
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
]
- def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
- output: Dict[int, List[int]] = {}
- for item_name, quantity in dict(self._mandatory_items.items()).items():
- item = self.item_data[item_name]
- if isinstance(item.definition, ProgressiveItemDefinition):
- # Note: we need to reference the static table here rather than the player-specific one because the child
- # items were removed from the pool when we pruned out all progression items not in the options.
- output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code)
- for child_item in item.definition.child_item_names]
- return output
+ def get_progressive_item_ids(self) -> Dict[int, List[int]]:
+ """
+ Returns a dict from progressive item IDs to the list of IDs of the base item that they unlock, in order.
+ """
+ return {
+ cast_not_none(static_witness_items.ITEM_DATA[progressive_item].ap_code): [
+ cast_not_none(static_witness_items.ITEM_DATA[base_item].ap_code)
+ for base_item in corresponding_base_items
+ ]
+ for progressive_item, corresponding_base_items in self.all_progressive_item_lists.items()
+ }
diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py
index b24434732ffc..c369ec623aa3 100644
--- a/worlds/witness/player_logic.py
+++ b/worlds/witness/player_logic.py
@@ -22,6 +22,7 @@
from .data import static_logic as static_witness_logic
from .data.definition_classes import ConnectionDefinition, WitnessRule
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
+from .data.settings.progressive_items import PROGRESSIVE_SYMBOLS
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import (
get_boat,
@@ -74,12 +75,12 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in
self.UNREACHABLE_REGIONS: Set[str] = set()
self.THEORETICAL_BASE_ITEMS: Set[str] = set()
- self.THEORETICAL_ITEMS: Set[str] = set()
- self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
+ self.THEORETICAL_PROGRESSIVE_LISTS: Dict[str, List[str]] = {}
+ self.ENABLED_PROGRESSIVE_LISTS: Dict[str, List[str]] = {}
+ self.FINALIZED_PROGRESSIVE_LISTS: Dict[str, List[str]] = {}
+ self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = {}
+ self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() # No "progressive" conversion yet
self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
-
- self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1)
- self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {}
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
self.FORBIDDEN_DOORS: Set[str] = set()
@@ -303,15 +304,13 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None:
line_split = line.split(" - ")
item_name = line_split[0]
+ # Do not add progressive items, delete the individual items
+ assert not isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition)
+
if item_name not in static_witness_items.ITEM_DATA:
raise RuntimeError(f'Item "{item_name}" does not exist.')
- self.THEORETICAL_ITEMS.add(item_name)
- if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
- self.THEORETICAL_BASE_ITEMS.update(cast(ProgressiveItemDefinition,
- static_witness_logic.ALL_ITEMS[item_name]).child_item_names)
- else:
- self.THEORETICAL_BASE_ITEMS.add(item_name)
+ self.THEORETICAL_BASE_ITEMS.add(item_name)
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
@@ -323,13 +322,10 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None:
if adj_type == "Remove Items":
item_name = line
- self.THEORETICAL_ITEMS.discard(item_name)
- if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
- self.THEORETICAL_BASE_ITEMS.difference_update(
- cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names
- )
- else:
- self.THEORETICAL_BASE_ITEMS.discard(item_name)
+ self.THEORETICAL_BASE_ITEMS.discard(item_name)
+
+ # Do not delete progressive items, delete the individual items
+ assert not isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition)
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
@@ -537,6 +533,52 @@ def handle_panelhunt_postgame(self, world: "WitnessWorld") -> List[List[str]]:
return postgame_adjustments
+ def add_implicit_dependencies_to_requirements(self, dependencies: Dict[str, str]) -> None:
+ if not dependencies:
+ return
+
+ for entity, requirement in self.DEPENDENT_REQUIREMENTS_BY_HEX.items():
+ if "items" not in requirement:
+ continue
+
+ new_requirement_options = set()
+ for requirement_option in requirement["items"]:
+ changed_requirement_option = set(requirement_option)
+ for item1, item2 in dependencies.items():
+ if item1 in requirement_option:
+ changed_requirement_option.add(item2)
+ new_requirement_options.add(frozenset(changed_requirement_option))
+ self.DEPENDENT_REQUIREMENTS_BY_HEX[entity]["items"] = frozenset(new_requirement_options)
+
+ def adjust_requirements_for_second_stage_symbols(self, world: "WitnessWorld") -> None:
+ """
+ When playing with non-progressive symbols,
+ there are some second-stage symbols that can't be used without the first-stage symbol.
+ However, there is a player option that makes these second stage symbols independent.
+
+ If they are independent, we rename "Dots" to "Sparse Dots" and "Stars" to "Simple Stars"
+ to drive home the separation of the respective items.
+
+ If they are not independent, we need to add the "Dots" requirement to every "Full Dots" panel,
+ as well as the "Stars" requirement to every "Stars + Same Colored Symbol" panel.
+
+ Also, if Progressive Symmetry is off and independent symbols are off, a Symmetry requirement is added to the
+ Symmetry Laser sets.
+ """
+
+ implicit_dependencies = {}
+
+ if "Full Dots" not in world.options.second_stage_symbols_act_independently:
+ implicit_dependencies["Full Dots"] = "Dots"
+
+ if "Stars + Same Colored Symbol" not in world.options.second_stage_symbols_act_independently:
+ implicit_dependencies["Stars + Same Colored Symbol"] = "Stars"
+
+ if "Colored Dots" not in world.options.second_stage_symbols_act_independently:
+ implicit_dependencies["Colored Dots"] = "Symmetry"
+
+ self.add_implicit_dependencies_to_requirements(implicit_dependencies)
+
def set_easter_egg_requirements(self, world: "WitnessWorld") -> None:
eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step()
@@ -649,6 +691,28 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None:
if world.options.shuffle_symbols:
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
+ self.THEORETICAL_PROGRESSIVE_LISTS = copy.deepcopy(PROGRESSIVE_SYMBOLS)
+
+ self.adjust_requirements_for_second_stage_symbols(world)
+
+ if world.options.colored_dots_are_progressive_dots:
+ # Insert after Dots
+ dots_index = self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].index("Dots")
+ self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].insert(dots_index + 1, "Colored Dots")
+
+ # Remove from Progressive Symmetry
+ self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Symmetry"].remove("Colored Dots")
+
+ if world.options.sound_dots_are_progressive_dots:
+ # Insert before Full Dots
+ full_dots_index = self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].index("Full Dots")
+ self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].insert(full_dots_index, "Sound Dots")
+
+ self.ENABLED_PROGRESSIVE_LISTS = {
+ progressive_item: item_list for progressive_item, item_list in self.THEORETICAL_PROGRESSIVE_LISTS.items()
+ if progressive_item in world.options.progressive_symbols
+ }
+
if world.options.EP_difficulty == "normal":
adjustment_linesets_in_order.append(get_ep_easy())
elif world.options.EP_difficulty == "tedious":
@@ -962,18 +1026,32 @@ def finalize_items(self) -> None:
"""
Finalise which items are used in the world, and handle their progressive versions.
"""
- for item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME:
- if item not in self.THEORETICAL_ITEMS:
- progressive_item_name = static_witness_logic.get_parent_progressive_item(item)
- self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
- child_items = cast(ProgressiveItemDefinition,
- static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names
- progressive_list = [child_item for child_item in child_items
- if child_item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME]
- self.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] = progressive_list.index(item) + 1
- self.PROGRESSIVE_LISTS[progressive_item_name] = progressive_list
- else:
- self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
+
+ self.FINALIZED_PROGRESSIVE_LISTS = self.ENABLED_PROGRESSIVE_LISTS.copy()
+
+ # Filter non existent base items
+ self.FINALIZED_PROGRESSIVE_LISTS = {
+ progressive_item: [
+ item for item in base_items if item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME
+ ]
+ for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items()
+ }
+
+ # Filter empty chains / chains with only one item (no point in having those)
+ self.FINALIZED_PROGRESSIVE_LISTS = {
+ progressive_item: base_items
+ for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items()
+ if len(base_items) >= 2 # No point in a single-item progressive chain
+ }
+
+ # Build PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME with the finalized progressive item replacements in mind
+ self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME = self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.copy()
+ for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items():
+ self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item)
+ self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME -= set(base_items)
+
+ for i, base_item in enumerate(base_items):
+ self.PARENT_ITEM_COUNT_PER_BASE_ITEM[base_item] = i + 1
def solvability_guaranteed(self, entity_hex: str) -> bool:
return not (
diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py
index 81dd28d68d09..170df82172b9 100644
--- a/worlds/witness/presets.py
+++ b/worlds/witness/presets.py
@@ -35,7 +35,7 @@
"challenge_lasers": 11,
"early_caves": EarlyCaves.option_off,
-
+ "early_good_items": {"Door / Door Panel"},
"elevators_come_to_you": ElevatorsComeToYou.default,
"trap_percentage": TrapPercentage.default,
@@ -76,7 +76,7 @@
"challenge_lasers": 9,
"early_caves": EarlyCaves.option_off,
-
+ "early_good_items": {"Symbol", "Door / Door Panel"}, # Not Obelisk Key bc I want EPs to open slowly in this one
"elevators_come_to_you": ElevatorsComeToYou.default,
"trap_percentage": TrapPercentage.default,
@@ -117,13 +117,13 @@
"challenge_lasers": 9,
"early_caves": EarlyCaves.option_off,
-
+ "early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"},
"elevators_come_to_you": ElevatorsComeToYou.valid_keys,
"trap_percentage": TrapPercentage.default,
"puzzle_skip_amount": 15,
"trap_weights": TrapWeights.default,
-
+
"hint_amount": HintAmount.default,
"area_hint_percentage": AreaHintPercentage.default,
"laser_hints": LaserHints.default,
diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py
index 545c3e7dd042..5822aadacfa7 100644
--- a/worlds/witness/rules.py
+++ b/worlds/witness/rules.py
@@ -203,10 +203,7 @@ def _has_item(item: str, world: "WitnessWorld",
if item == "Theater to Tunnels":
return lambda state: _can_do_theater_to_tunnels(state, world)
- actual_item = static_witness_logic.get_parent_progressive_item(item)
- needed_amount = player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM[item]
-
- simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(actual_item, needed_amount)
+ simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(item, 1)
return simple_rule
@@ -214,6 +211,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S
-> List[Union[CollectionRule, SimpleItemRepresentation]]:
"""
This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version.
+ It is unclear how much this does after the recent rework the progressive items, but there is no reason to remove it.
"""
direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)]
@@ -231,12 +229,15 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S
def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]],
- player: int) -> List[CollectionRule]:
+ world: "WitnessWorld") -> List[CollectionRule]:
"""
Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules.
If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts()
"""
+ player_logic = world.player_logic
+ player = world.player
+
collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)]
item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)]
@@ -251,7 +252,7 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte
# Sort the list by which item you are least likely to have (E.g. last stage of progressive item chains)
sorted_item_list = sorted(
item_counts.keys(),
- key=lambda item_name: item_counts[item_name] if ("Progressive" in item_name) else 1.5,
+ key=lambda item_name: player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM.get(item_name, 1.5),
reverse=True
# 1.5 because you are less likely to have a single stage item than one copy of a 2-stage chain
# I did some testing and every part of this genuinely gives a tiiiiny performance boost over not having it!
@@ -272,8 +273,6 @@ def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -
"""
Converts a WitnessRule into a CollectionRule.
"""
- player = world.player
-
if requirements == frozenset({frozenset()}):
return None
@@ -284,7 +283,7 @@ def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -
optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion]
- fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion]
+ fully_converted_rules = [convert_requirement_option(sublist, world) for sublist in optimized_rule_conversion]
if len(fully_converted_rules) == 1:
if len(fully_converted_rules[0]) == 1:
diff --git a/worlds/witness/test/bases.py b/worlds/witness/test/bases.py
index c3b427851af0..97f3ff921928 100644
--- a/worlds/witness/test/bases.py
+++ b/worlds/witness/test/bases.py
@@ -122,6 +122,20 @@ def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]
)
item_objects.append(removed_item)
+ def assert_quantities_in_itempool(self, expected_quantities: Mapping[str, int]) -> None:
+ for item, expected_quantity in expected_quantities.items():
+ with self.subTest(f"Verify that there are {expected_quantity} copies of {item} in the itempool."):
+ found_items = self.get_items_by_name(item)
+ self.assertEqual(len(found_items), expected_quantity)
+
+ def assert_item_exists_and_is_proguseful(self, item_name: str, proguseful=True):
+ items = self.get_items_by_name(item_name)
+ self.assertTrue(items)
+ if proguseful:
+ self.assertTrue(all(item.advancement and item.useful for item in items))
+ else:
+ self.assertTrue(all(item.advancement and not item.useful for item in items))
+
class WitnessMultiworldTestBase(MultiworldTestBase):
options_per_world: List[Dict[str, Any]]
diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py
index 00071ec5f6f0..e999fb452d66 100644
--- a/worlds/witness/test/test_disable_non_randomized.py
+++ b/worlds/witness/test/test_disable_non_randomized.py
@@ -8,7 +8,7 @@ class TestDisableNonRandomized(WitnessTestBase):
options = {
"disable_non_randomized_puzzles": True,
"shuffle_doors": "panels",
- "early_symbol_item": False,
+ "early_good_items": {},
}
def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None:
diff --git a/worlds/witness/test/test_early_good_item.py b/worlds/witness/test/test_early_good_item.py
new file mode 100644
index 000000000000..1641b0f1b005
--- /dev/null
+++ b/worlds/witness/test/test_early_good_item.py
@@ -0,0 +1,109 @@
+from BaseClasses import ItemClassification, LocationProgressType
+from Fill import distribute_items_restrictive
+
+from ..data.utils import cast_not_none
+from ..test.bases import WitnessTestBase
+
+
+class TestEarlySymbolItemFalse(WitnessTestBase):
+ options = {
+ "early_good_items": {},
+
+ "shuffle_symbols": True,
+ "shuffle_doors": "off",
+ "shuffle_boat": False,
+ "shuffle_lasers": False,
+ "obelisk_keys": False,
+ }
+
+ def setUp(self) -> None:
+ if self.auto_construct:
+ self.world_setup(seed=1) # Magic seed to prevent false positive
+
+ def test_early_good_item(self) -> None:
+ distribute_items_restrictive(self.multiworld)
+
+ gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
+
+ self.assertFalse(
+ cast_not_none(gate_open.item).classification & ItemClassification.progression,
+ "Early Good Item was off, yet a Symbol item ended up on Tutorial Gate Open.",
+ )
+
+
+class TestEarlySymbolItemTrue(WitnessTestBase):
+ options = {
+ "early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"},
+
+ "shuffle_symbols": True,
+ "shuffle_doors": "panels",
+ "shuffle_EPs": "individual",
+ "obelisk_keys": True,
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+
+ def test_early_good_item(self) -> None:
+ """
+ The items should be in the order:
+ Symbol item on Tutorial Gate Open
+ Door item on Tutorial Back Left
+ Obelisk Key on Tutorial Back Right
+ """
+
+ distribute_items_restrictive(self.multiworld)
+
+ gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
+
+ self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.")
+
+ self.assertTrue(
+ cast_not_none(gate_open.item).name in self.world.item_name_groups["Symbols"],
+ "Early Good Item was on, yet no Symbol item ended up on Tutorial Gate Open.",
+ )
+
+ back_left = self.multiworld.get_location("Tutorial Back Left", 1)
+
+ self.assertTrue(back_left.item is not None, "Somehow, no item got placed on Tutorial Back Left.")
+
+ doors_and_panel_keys = self.world.item_name_groups["Doors"] | self.world.item_name_groups["Panel Keys"]
+ self.assertTrue(
+ cast_not_none(back_left.item).name in doors_and_panel_keys,
+ "Early Good Item was on, yet no Door item ended up on Tutorial Back Left.",
+ )
+
+ back_right = self.multiworld.get_location("Tutorial Back Right", 1)
+
+ self.assertTrue(back_right.item is not None, "Somehow, no item got placed on Tutorial Back Right.")
+
+ self.assertTrue(
+ cast_not_none(back_right.item).name in self.world.item_name_groups["Obelisk Keys"],
+ "Early Good Item was on, yet no Obelisk Key item ended up on Tutorial Back Right.",
+ )
+
+
+class TestEarlySymbolItemTrueButExcluded(WitnessTestBase):
+ options = {
+ "shuffle_symbols": True,
+ "shuffle_doors": "off",
+ "shuffle_boat": False,
+ "shuffle_lasers": False,
+ "obelisk_keys": False,
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.multiworld.get_location("Tutorial Gate Open", 1).progress_type = LocationProgressType.EXCLUDED
+
+ def test_early_good_item(self) -> None:
+ distribute_items_restrictive(self.multiworld)
+
+ gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
+
+ self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.")
+
+ self.assertFalse(
+ cast_not_none(gate_open.item).classification & ItemClassification.progression,
+ "Tutorial Gate Open was excluded, yet it still received an early Symbol item.",
+ )
diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py
index 4a71c0d433be..be71979458e8 100644
--- a/worlds/witness/test/test_lasers.py
+++ b/worlds/witness/test/test_lasers.py
@@ -7,7 +7,7 @@ class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase):
"puzzle_randomization": "sigma_normal",
"mountain_lasers": 1,
"victory_condition": "elevator",
- "early_symbol_item": False,
+ "early_good_items": {},
}
def test_symbols_to_win(self) -> None:
@@ -37,7 +37,7 @@ class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
- "early_symbol_item": False,
+ "early_good_items": {},
"puzzle_randomization": "sigma_expert",
}
@@ -70,7 +70,7 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
- "early_symbol_item": False,
+ "early_good_items": {},
"puzzle_randomization": "none",
}
@@ -101,7 +101,6 @@ class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
- "early_symbol_item": False,
"puzzle_randomization": "umbra_variety",
}
@@ -134,7 +133,7 @@ class TestPanelsRequiredToWinElevator(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
- "early_symbol_item": False,
+ "early_good_items": {},
"shuffle_symbols": False,
"shuffle_doors": "panels",
"door_groupings": "off",
@@ -163,7 +162,7 @@ class TestDoorsRequiredToWinElevator(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
- "early_symbol_item": False,
+ "early_good_items": {},
"shuffle_symbols": False,
"shuffle_doors": "doors",
"door_groupings": "off",
diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py
index fb1d82081594..f40e1b30362f 100644
--- a/worlds/witness/test/test_symbol_shuffle.py
+++ b/worlds/witness/test/test_symbol_shuffle.py
@@ -1,38 +1,394 @@
from ..test.bases import WitnessMultiworldTestBase, WitnessTestBase
-class TestSymbols(WitnessTestBase):
+class TestProgressiveSymbols(WitnessTestBase):
options = {
- "early_symbol_item": False,
+ "early_good_items": {},
+ "puzzle_randomization": "umbra_variety",
+ "progressive_symbols": {
+ "Progressive Dots",
+ "Progressive Symmetry",
+ "Progressive Stars",
+ "Progressive Squares",
+ "Progressive Shapers",
+ "Progressive Discard Symbols"
+ }
}
def test_progressive_symbols(self) -> None:
"""
- Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots,
+ Test that Full Dots are correctly replaced by 2x Progressive Dots,
and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively.
"""
+ expected_quantities = {
+ # Individual items that are replaced by progressive items
+ "Dots": 0,
+ "Sparse Dots": 0,
+ "Full Dots": 0,
+ "Symmetry": 0,
+ "Colored Dots": 0,
+ "Stars": 0,
+ "Simple Stars": 0,
+ "Stars + Same Colored Symbol": 0,
+ "Black/White Squares": 0,
+ "Colored Squares": 0,
+ "Shapers": 0,
+ "Rotated Shapers": 0,
+ "Negative Shapers": 0,
+ "Triangles": 0,
+ "Arrows": 0,
+
+ # Progressive items
+ "Progressive Dots": 2,
+ "Progressive Symmetry": 2,
+ "Progressive Stars": 2,
+ "Progressive Squares": 2,
+ "Progressive Shapers": 3,
+ "Progressive Discard Symbols": 2,
+
+ # Individual items that still exist because they aren't a part of any progressive chain
+ "Sound Dots": 1,
+ }
+
+ self.assert_quantities_in_itempool(expected_quantities)
+
+ with self.subTest("Verify that Dots panels need 1 copy of Progressive Dots and Full Dots panel need 2 copies"):
+ self.collect_all_but("Progressive Dots")
+ progressive_dots = self.get_items_by_name("Progressive Dots")
+ self.assertEqual(len(progressive_dots), 2)
+
+ self.assertFalse(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+
+ self.collect(progressive_dots.pop())
+
+ self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+
+ self.collect(progressive_dots.pop())
+
+ self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+
+ with self.subTest("Verify proguseful status of progressive & alias items"):
+ self.assert_item_exists_and_is_proguseful("Progressive Symmetry", proguseful=False)
+ self.assert_item_exists_and_is_proguseful("Sound Dots", proguseful=False)
+
+ self.assert_item_exists_and_is_proguseful("Progressive Dots")
+ self.assert_item_exists_and_is_proguseful("Progressive Stars")
+ self.assert_item_exists_and_is_proguseful("Progressive Squares")
+ self.assert_item_exists_and_is_proguseful("Progressive Shapers")
+ self.assert_item_exists_and_is_proguseful("Progressive Discard Symbols")
+
+
+class TestIndependentSecondStageSymbols(WitnessTestBase):
+ options = {
+ "early_good_items": {},
+ "puzzle_randomization": "umbra_variety",
+ "progressive_symbols": {},
+ "second_stage_symbols_act_independently": {
+ "Full Dots",
+ "Stars + Same Colored Symbol",
+ "Colored Dots",
+ },
+ "shuffle_doors": "doors",
+ }
+
+ def test_independent_second_stage_symbols(self) -> None:
+ expected_quantities = {
+ # Progressive items shouldn't exist
+ "Progressive Dots": 0,
+ "Progressive Symmetry": 0,
+ "Progressive Stars": 0,
+ "Progressive Squares": 0,
+ "Progressive Shapers": 0,
+ "Progressive Discard Symbols": 0,
+
+ # Dots and Stars are replaced by Sparse Dots and Simple Stars
+ "Dots": 0,
+ "Stars": 0,
+ "Sparse Dots": 1,
+ "Simple Stars": 1,
+
+ # None of the symbols are progressive, so they should all exist
+ "Full Dots": 1,
+ "Symmetry": 1,
+ "Colored Dots": 1,
+ "Stars + Same Colored Symbol": 1,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Negative Shapers": 1,
+ "Triangles": 1,
+ "Arrows": 1,
+ "Sound Dots": 1,
+ }
+
+ self.assert_quantities_in_itempool(expected_quantities)
+
+ with self.subTest("Verify that Full Dots panels only need Full Dots"):
+ self.collect_by_name("Black/White Squares")
+ self.collect_by_name("Triangles")
+ self.collect_by_name("Outside Tutorial Outpost Exit (Door)")
+
+ self.assertFalse(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)
+ )
+ self.collect_by_name("Full Dots")
+ self.assertTrue(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)
+ )
+
+ with self.subTest("Verify that Stars + Same Colored Symbol panels only need Stars + Same Colored Symbol"):
+ self.collect_by_name("Eraser")
+ self.collect_by_name("Quarry Entry 1 (Door)")
+ self.collect_by_name("Quarry Entry 2 (Door)")
+
+ self.assertFalse(
+ self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player)
+ )
+ self.collect_by_name("Stars + Same Colored Symbol")
+ self.assertTrue(
+ self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player)
+ )
+
+ with self.subTest("Verify that non-symmetry Colored Dots panels only need Colored Dots"):
+ self.collect_by_name("Symmetry Island Lower (Door)")
+ self.collect_by_name("Symmetry Island Upper (Door)")
+
+ self.assertFalse(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.collect_by_name("Colored Dots")
+ self.assertTrue(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+
+ with self.subTest("Verify proguseful status of progressive & alias items"):
+ self.assert_item_exists_and_is_proguseful("Full Dots", proguseful=False)
+ self.assert_item_exists_and_is_proguseful("Stars + Same Colored Symbol", proguseful=False)
+ self.assert_item_exists_and_is_proguseful("Arrows", proguseful=False) # Variety
+
+ self.assert_item_exists_and_is_proguseful("Sparse Dots")
+ self.assert_item_exists_and_is_proguseful("Simple Stars")
+ self.assert_item_exists_and_is_proguseful("Triangles") # Variety
+
+
+class TestDependentSecondStageSymbols(WitnessTestBase):
+ options = {
+ "early_good_items": {},
+ "puzzle_randomization": "umbra_variety",
+ "progressive_symbols": {},
+ "second_stage_symbols_act_independently": {},
+ "shuffle_doors": "doors",
+ }
+
+ def test_dependent_second_stage_symbols(self) -> None:
+ expected_quantities = {
+ # Progressive items shouldn't exist
+ "Progressive Dots": 0,
+ "Progressive Symmetry": 0,
+ "Progressive Stars": 0,
+ "Progressive Squares": 0,
+ "Progressive Shapers": 0,
+ "Progressive Discard Symbols": 0,
+
+ # Dots and Stars are NOT replaced by Sparse Dots and Simple Stars
+ "Dots": 1,
+ "Stars": 1,
+ "Sparse Dots": 0,
+ "Simple Stars": 0,
+
+ # None of the symbols are progressive, so they should all exist
+ "Full Dots": 1,
+ "Symmetry": 1,
+ "Colored Dots": 1,
+ "Stars + Same Colored Symbol": 1,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Negative Shapers": 1,
+ "Triangles": 1,
+ "Arrows": 1,
+ "Sound Dots": 1,
+ }
+
+ self.assert_quantities_in_itempool(expected_quantities)
+
+ with self.subTest("Verify that Full Dots panels need Dots as well"):
+ self.collect_by_name("Black/White Squares")
+ self.collect_by_name("Triangles")
+ self.collect_by_name("Outside Tutorial Outpost Exit (Door)")
+
+ self.assertFalse(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player))
+ self.collect_by_name("Full Dots")
+ self.assertFalse(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player))
+ self.collect_by_name("Dots")
+ self.assertTrue(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)
+ )
+
+ with self.subTest("Verify that Stars + Same Colored Symbol panels need Stars as well"):
+ self.collect_by_name("Eraser")
+ self.collect_by_name("Quarry Entry 1 (Door)")
+ self.collect_by_name("Quarry Entry 2 (Door)")
+
+ self.assertFalse(
+ self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player)
+ )
+ self.collect_by_name("Stars + Same Colored Symbol")
+ self.assertFalse(
+ self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player)
+ )
+ self.collect_by_name("Stars")
+ self.assertTrue(
+ self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player)
+ )
+
+ with self.subTest("Verify that non-symmetry Colored Dots panels need Symmetry as well"):
+ self.collect_by_name("Symmetry Island Lower (Door)")
+ self.collect_by_name("Symmetry Island Upper (Door)")
+
+ self.assertFalse(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.collect_by_name("Colored Dots")
+ self.assertFalse(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.collect_by_name("Symmetry")
+ self.assertTrue(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+
+ with self.subTest("Verify proguseful status of progressive & alias items"):
+ self.assert_item_exists_and_is_proguseful("Full Dots", proguseful=False)
+ self.assert_item_exists_and_is_proguseful("Stars + Same Colored Symbol", proguseful=False)
+ self.assert_item_exists_and_is_proguseful("Arrows", proguseful=False) # Variety
+
+ self.assert_item_exists_and_is_proguseful("Dots")
+ self.assert_item_exists_and_is_proguseful("Stars")
+ self.assert_item_exists_and_is_proguseful("Triangles") # Variety
+
+
+class TestAlternateProgressiveDots(WitnessTestBase):
+ options = {
+ "early_good_items": {},
+ "puzzle_randomization": "umbra_variety",
+ "progressive_symbols": {
+ "Progressive Dots",
+ "Progressive Symmetry"
+ },
+ "second_stage_symbols_act_independently": {
+ "Full Dots",
+ "Stars + Same Colored Symbol",
+ "Colored Dots",
+ },
+ "colored_dots_are_progressive_dots": True,
+ "sound_dots_are_progressive_dots": True,
+ "shuffle_doors": "doors",
+ }
+
+ def test_alternate_progressive_dots(self) -> None:
+ expected_quantities = {
+ # Progressive Dots chain now has 4 members
+ "Progressive Dots": 4,
+
+ # Dots items don't exist because Progressive Dots is on.
+ # For this test, this includes Colored Dots and Sound Dots as well
+ "Dots": 0,
+ "Sparse Dots": 0,
+ "Colored Dots": 0,
+ "Sound Dots": 0,
+ "Full Dots": 0,
+
+ # Progressive Symmetry no longer exists, because Colored Dots is part of the Progressive Dots chain instead
+ "Progressive Symmetry": 0,
+
+ # Other Progressive Symbols don't exist
+ "Progressive Stars": 0,
+ "Progressive Squares": 0,
+ "Progressive Shapers": 0,
+ "Progressive Discard Symbols": 0,
+
+ # Other standalone items exist, because they are not progressive
+ "Symmetry": 1,
+ "Stars + Same Colored Symbol": 1,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Negative Shapers": 1,
+ "Triangles": 1,
+ "Arrows": 1,
+
+ # This test is set to have independent symbols, so Simple Stars exist instead of Stars
+ "Stars": 0,
+ "Simple Stars": 1,
+ }
+
+ self.assert_quantities_in_itempool(expected_quantities)
+
+ self.collect_all_but(["Progressive Dots", "Symmetry"]) # Skip Symmetry so we can also test a little quirk
progressive_dots = self.get_items_by_name("Progressive Dots")
- self.assertEqual(len(progressive_dots), 2)
+ self.assertEqual(len(progressive_dots), 4)
+
+ with self.subTest("Test that one copy of Progressive Dots unlocks Dots panels"):
+ self.assertFalse(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertFalse(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+
+ self.collect(progressive_dots.pop())
+
+ self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertFalse(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+
+ with self.subTest("Test that two copies of Progressive Dots unlocks Colored Dots panels"):
+ self.collect(progressive_dots.pop())
+
+ self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ # Also test here that these "Colored Dots" act independently from Symmetry like they are supposed to
+ self.assertTrue(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
- self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
- self.assertFalse(
- self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
- )
+ with self.subTest("Test that three copies of Progressive Dots unlocks Sound Dots panels"):
+ self.collect(progressive_dots.pop())
- self.collect(progressive_dots.pop())
+ self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertTrue(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.assertTrue(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
- self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
- self.assertFalse(
- self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
- )
+ with self.subTest("Test that four copies of Progressive Dots unlocks Full Dots panels"):
+ self.collect(progressive_dots.pop())
- self.collect(progressive_dots.pop())
+ self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player))
+ self.assertTrue(
+ self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player)
+ )
+ self.assertTrue(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
- self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
- self.assertTrue(
- self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
- )
+ with self.subTest("Verify proguseful status of progressive & alias items"):
+ self.assert_item_exists_and_is_proguseful("Progressive Dots")
+ self.assert_item_exists_and_is_proguseful("Simple Stars")
class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
@@ -53,7 +409,7 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
common_options = {
"shuffle_discarded_panels": True,
- "early_symbol_item": False,
+ "early_good_items": {},
}
def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: