mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-22 14:02:01 -07:00
Merge remote-tracking branch 'remotes/upstream/main'
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"include": [
|
||||
"../BizHawkClient.py",
|
||||
"../Patch.py",
|
||||
"../test/param.py",
|
||||
"../test/general/test_groups.py",
|
||||
"../test/general/test_helpers.py",
|
||||
"../test/general/test_memory.py",
|
||||
|
||||
@@ -36,9 +36,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
if: startsWith(matrix.os,'windows')
|
||||
- uses: Bacondish2023/setup-googletest@v1
|
||||
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
||||
with:
|
||||
build-type: 'Release'
|
||||
- name: Build tests
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.aptww
|
||||
*.apemerald
|
||||
*.pyc
|
||||
*.pyd
|
||||
|
||||
+1
-1
@@ -511,7 +511,7 @@ if __name__ == '__main__':
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
+1
-1
@@ -1128,7 +1128,7 @@ def run_as_textclient(*args):
|
||||
args = handle_url_arg(args, parser=parser)
|
||||
|
||||
# use colorama to display colored text highlighting on windows
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ if __name__ == '__main__':
|
||||
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -506,7 +506,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
la_task = None
|
||||
client = None
|
||||
# TODO: does this need to re-read on reset?
|
||||
found_checks = []
|
||||
found_checks = set()
|
||||
last_resend = time.time()
|
||||
|
||||
magpie_enabled = False
|
||||
@@ -558,10 +558,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
self.ui = LADXManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def send_checks(self):
|
||||
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||
# Store the entrances we find on the server for future sessions
|
||||
@@ -613,8 +609,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
self.client.pending_deathlink = True
|
||||
|
||||
def new_checks(self, item_ids, ladxr_ids):
|
||||
self.found_checks += item_ids
|
||||
create_task_log_exception(self.send_checks())
|
||||
self.found_checks.update(item_ids)
|
||||
create_task_log_exception(self.check_locations(self.found_checks))
|
||||
if self.magpie_enabled:
|
||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||
|
||||
@@ -721,7 +717,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
if self.last_resend + 5.0 < now:
|
||||
self.last_resend = now
|
||||
await self.send_checks()
|
||||
await self.check_locations(self.found_checks)
|
||||
if self.magpie_enabled:
|
||||
try:
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
@@ -803,6 +799,6 @@ async def main():
|
||||
await ctx.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
+1
-1
@@ -370,7 +370,7 @@ if __name__ == "__main__":
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
min_client_version = Version(0, 1, 6)
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
|
||||
def remove_from_list(container, value):
|
||||
|
||||
+1
-1
@@ -346,7 +346,7 @@ if __name__ == '__main__':
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
+2
-1
@@ -1579,6 +1579,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
player_output = {
|
||||
"Game": multiworld.game[player],
|
||||
"Name": multiworld.get_player_name(player),
|
||||
"ID": player,
|
||||
}
|
||||
output.append(player_output)
|
||||
for option_key, option in world.options_dataclass.type_hints.items():
|
||||
@@ -1591,7 +1592,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
game_option_names.append(display_name)
|
||||
|
||||
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
|
||||
fields = ["Game", "Name", *all_option_names]
|
||||
fields = ["ID", "Game", "Name", *all_option_names]
|
||||
writer = DictWriter(file, fields)
|
||||
writer.writeheader()
|
||||
writer.writerows(output)
|
||||
|
||||
@@ -81,6 +81,7 @@ Currently, the following games are supported:
|
||||
* Castlevania: Circle of the Moon
|
||||
* Inscryption
|
||||
* Civilization VI
|
||||
* The Legend of Zelda: The Wind Waker
|
||||
* Jak and Daxter: The Precursor Legacy
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
|
||||
+1
-1
@@ -735,6 +735,6 @@ async def main() -> None:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
+1
-1
@@ -500,7 +500,7 @@ def main():
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(_main())
|
||||
colorama.deinit()
|
||||
|
||||
+1
-1
@@ -446,6 +446,6 @@ if __name__ == '__main__':
|
||||
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
flask>=3.0.3
|
||||
werkzeug>=3.0.6
|
||||
flask>=3.1.0
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.0
|
||||
waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Flask-Compress>=1.17
|
||||
Flask-Limiter>=3.12
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RandomizeButton(option_name, option) %}
|
||||
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
|
||||
<div class="randomize-button" data-tooltip="Pick a random value for this option.">
|
||||
<label for="random-{{ option_name }}">
|
||||
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
|
||||
🎲
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
{% else %}
|
||||
<div class="unsupported-option">
|
||||
This option is not supported. Please edit your .yaml file manually.
|
||||
This option cannot be modified here. Please edit your .yaml file manually.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
+1
-1
@@ -386,7 +386,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a Archipelago Binary Patch file')
|
||||
args = parser.parse_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -217,6 +217,9 @@
|
||||
# Wargroove
|
||||
/worlds/wargroove/ @FlySniper
|
||||
|
||||
# The Wind Waker
|
||||
/worlds/tww/ @tanjo3
|
||||
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
|
||||
@@ -82,6 +82,38 @@ Unit tests can also be created using [TestBase](/test/bases.py#L16) or
|
||||
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
|
||||
testing portions of your code that can be tested without relying on a multiworld to be created first.
|
||||
|
||||
#### Parametrization
|
||||
|
||||
When defining a test that needs to cover a range of inputs it is useful to parameterize (to run the same test
|
||||
for multiple inputs) the base test. Some important things to consider when attempting to parametrize your test are:
|
||||
|
||||
* [Subtests](https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests)
|
||||
can be used to have parametrized assertions that show up similar to individual tests but without the overhead
|
||||
of needing to instantiate multiple tests; however, subtests can not be multithreaded and do not have individual
|
||||
timing data, so they are not suitable for slow tests.
|
||||
|
||||
* Archipelago's tests are test-runner-agnostic. That means tests are not allowed to use e.g. `@pytest.mark.parametrize`.
|
||||
Instead, we define our own parametrization helpers in [test.param](/test/param.py).
|
||||
|
||||
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
|
||||
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
|
||||
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly
|
||||
or setting `WorldTestBase.run_default_tests` to False.
|
||||
|
||||
#### Performance Considerations
|
||||
|
||||
Archipelago is big enough that the runtime of unittests can have an impact on productivity.
|
||||
|
||||
Individual tests should take less than a second, so they can be properly multithreaded.
|
||||
|
||||
Ideally, thorough tests are directed at actual code/functionality. Do not just create and/or fill a ton of individual
|
||||
Multiworlds that spend most of the test time outside what you actually want to test.
|
||||
|
||||
Consider generating/validating "random" games as part of your APWorld release workflow rather than having that be part
|
||||
of continuous integration, and add minimal reproducers to the "normal" tests for problems that were found.
|
||||
You can use [@unittest.skipIf](https://docs.python.org/3/library/unittest.html#unittest.skipIf) with an environment
|
||||
variable to keep all the benefits of the test framework while not running the marked tests by default.
|
||||
|
||||
## Running Tests
|
||||
|
||||
#### Using Pycharm
|
||||
@@ -100,3 +132,11 @@ next to the run and debug buttons.
|
||||
#### Running Tests without Pycharm
|
||||
|
||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||
|
||||
#### Running Tests Multithreaded
|
||||
|
||||
pytest can run multiple test runners in parallel with the pytest-xdist extension.
|
||||
|
||||
Install with `pip install pytest-xdist`.
|
||||
|
||||
Run with `pytest -n12` to spawn 12 process that each run 1/12th of the tests.
|
||||
|
||||
+42
-8
@@ -265,14 +265,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
|
||||
return { group: get_target_groups(group) for group in unique_groups }
|
||||
|
||||
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
|
||||
one_way_target_name: str | None = None) -> None:
|
||||
"""
|
||||
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance.
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
|
||||
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
|
||||
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
|
||||
|
||||
:param entrance: The entrance which will be disconnected in preparation for randomization.
|
||||
:param target_group: The group to assign to the created ER target. If not specified, the group from
|
||||
the original entrance will be copied.
|
||||
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
|
||||
is required for one-way entrances and is ignored otherwise.
|
||||
"""
|
||||
child_region = entrance.connected_region
|
||||
parent_region = entrance.parent_region
|
||||
@@ -287,8 +292,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
|
||||
# targets in the child region will be created when the other direction edge is disconnected
|
||||
target = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
# for 1-ways, the child region needs a target and coupling/naming is not a concern
|
||||
target = child_region.create_er_target(child_region.name)
|
||||
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
|
||||
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
|
||||
if not one_way_target_name:
|
||||
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
|
||||
target = child_region.create_er_target(one_way_target_name)
|
||||
target.randomization_type = entrance.randomization_type
|
||||
target.randomization_group = target_group or entrance.randomization_group
|
||||
|
||||
@@ -358,6 +366,34 @@ def randomize_entrances(
|
||||
if on_connect:
|
||||
on_connect(er_state, placed_exits)
|
||||
|
||||
def needs_speculative_sweep(dead_end: bool, require_new_exits: bool, placeable_exits: list[Entrance]) -> bool:
|
||||
# speculative sweep is expensive. We currently only do it as a last resort, if we might cap off the graph
|
||||
# entirely
|
||||
if len(placeable_exits) > 1:
|
||||
return False
|
||||
|
||||
# in certain stages of randomization we either expect or don't care if the search space shrinks.
|
||||
# we should never speculative sweep here.
|
||||
if dead_end or not require_new_exits or not perform_validity_check:
|
||||
return False
|
||||
|
||||
# edge case - if all dead ends have pre-placed progression or indirect connections, they are pulled forward
|
||||
# into the non dead end stage. In this case, and only this case, it's possible that the last connection may
|
||||
# actually be placeable in stage 1. We need to skip speculative sweep in this case because we expect the graph
|
||||
# to get capped off.
|
||||
|
||||
# check to see if we are proposing the last placement
|
||||
if not coupled:
|
||||
# in uncoupled, this check is easy as there will only be one target.
|
||||
is_last_placement = len(entrance_lookup) == 1
|
||||
else:
|
||||
# a bit harder, there may be 1 or 2 targets depending on if the exit to place is one way or two way.
|
||||
# if it is two way, we can safely assume that one of the targets is the logical pair of the exit.
|
||||
desired_target_count = 2 if placeable_exits[0].randomization_type == EntranceType.TWO_WAY else 1
|
||||
is_last_placement = len(entrance_lookup) == desired_target_count
|
||||
# if it's not the last placement, we need a sweep
|
||||
return not is_last_placement
|
||||
|
||||
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
|
||||
nonlocal perform_validity_check
|
||||
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
|
||||
@@ -371,11 +407,9 @@ def randomize_entrances(
|
||||
# very last exit and check whatever exits we open up are functionally accessible.
|
||||
# this requirement can be ignored on a beaten minimal, islands are no issue there.
|
||||
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
|
||||
or target_entrance.connected_region not in er_state.placed_regions)
|
||||
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
|
||||
and len(placeable_exits) == 1)
|
||||
or target_entrance.connected_region not in er_state.placed_regions)
|
||||
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
|
||||
if (needs_speculative_sweep
|
||||
if (needs_speculative_sweep(dead_end, require_new_exits, placeable_exits)
|
||||
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
|
||||
continue
|
||||
do_placement(source_exit, target_entrance)
|
||||
|
||||
+9
-9
@@ -1,14 +1,14 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.0
|
||||
jinja2>=3.1.4
|
||||
jellyfish>=1.1.3
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.1.31
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
typing_extensions>=4.12.2
|
||||
|
||||
@@ -148,7 +148,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e)
|
||||
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -158,10 +158,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(1, r2.entrances[0].randomization_group)
|
||||
|
||||
def test_disconnect_default_1way_no_vanilla_target_raises(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
r2 = Region("r2", 1, multiworld)
|
||||
e = r1.create_exit("e")
|
||||
e.randomization_type = EntranceType.ONE_WAY
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
disconnect_entrance_for_randomization(e)
|
||||
|
||||
def test_disconnect_uses_alternate_group(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
@@ -171,7 +183,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e, 2)
|
||||
disconnect_entrance_for_randomization(e, 2, "foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -181,7 +193,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(2, r2.entrances[0].randomization_group)
|
||||
|
||||
@@ -218,7 +230,7 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
self.assertEqual(80, len(result.pairings))
|
||||
self.assertEqual(80, len(result.placements))
|
||||
|
||||
def test_coupling(self):
|
||||
def test_coupled(self):
|
||||
"""tests that in coupled mode, all 2 way transitions have an inverse"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
@@ -236,6 +248,36 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
# if we didn't visit every placement the verification on_connect doesn't really mean much
|
||||
self.assertEqual(len(result.placements), seen_placement_count)
|
||||
|
||||
def test_uncoupled_succeeds_stage1_indirect_condition(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
menu = multiworld.get_region("Menu", 1)
|
||||
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
|
||||
end = Region("End", 1, multiworld)
|
||||
multiworld.regions.append(end)
|
||||
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
|
||||
multiworld.register_indirect_condition(end, None)
|
||||
|
||||
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
|
||||
self.assertSetEqual({
|
||||
("Menu_right", "End_left"),
|
||||
("End_left", "Menu_right")
|
||||
}, set(result.pairings))
|
||||
|
||||
def test_coupled_succeeds_stage1_indirect_condition(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
menu = multiworld.get_region("Menu", 1)
|
||||
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
|
||||
end = Region("End", 1, multiworld)
|
||||
multiworld.regions.append(end)
|
||||
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
|
||||
multiworld.register_indirect_condition(end, None)
|
||||
|
||||
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
|
||||
self.assertSetEqual({
|
||||
("Menu_right", "End_left"),
|
||||
("End_left", "Menu_right")
|
||||
}, set(result.pairings))
|
||||
|
||||
def test_uncoupled(self):
|
||||
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
||||
multiworld = generate_test_multiworld()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from typing import List, Tuple
|
||||
from typing import ClassVar, List, Tuple
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
|
||||
from Options import Accessibility
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
||||
from ..general import gen_steps, setup_multiworld
|
||||
from ..param import classvar_matrix
|
||||
|
||||
|
||||
class MultiworldTestBase(TestCase):
|
||||
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
|
||||
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
|
||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
game: ClassVar[str]
|
||||
|
||||
def test_two_player_single_game_fills(self) -> None:
|
||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||
for world_type in AutoWorldRegister.world_types.values():
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
world_type = AutoWorldRegister.world_types[self.game]
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import itertools
|
||||
import sys
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
|
||||
def classvar_matrix(**kwargs: Iterable[Any]) -> Callable[[type], None]:
|
||||
"""
|
||||
Create a new class for each variation of input, allowing to generate a TestCase matrix / parametrization that
|
||||
supports multi-threading and has better reporting for ``unittest --durations=...`` and ``pytest --durations=...``
|
||||
than subtests.
|
||||
|
||||
The kwargs will be set as ClassVars in the newly created classes. Use as ::
|
||||
|
||||
@classvar_matrix(var_name=[value1, value2])
|
||||
class MyTestCase(unittest.TestCase):
|
||||
var_name: typing.ClassVar[...]
|
||||
|
||||
:param kwargs: A dict of ClassVars to set, where key is the variable name and value is a list of all values.
|
||||
:return: A decorator to be applied to a class.
|
||||
"""
|
||||
keys: tuple[str]
|
||||
values: Iterable[Iterable[Any]]
|
||||
keys, values = zip(*kwargs.items())
|
||||
values = map(lambda v: sorted(v) if isinstance(v, (set, frozenset)) else v, values)
|
||||
permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
|
||||
|
||||
def decorator(cls: type) -> None:
|
||||
mod = sys.modules[cls.__module__]
|
||||
|
||||
for permutation in permutations_dicts:
|
||||
|
||||
class Unrolled(cls): # type: ignore
|
||||
pass
|
||||
|
||||
for k, v in permutation.items():
|
||||
setattr(Unrolled, k, v)
|
||||
params = ", ".join([f"{k}={repr(v)}" for k, v in permutation.items()])
|
||||
params = f"{{{params}}}"
|
||||
|
||||
Unrolled.__module__ = cls.__module__
|
||||
Unrolled.__qualname__ = f"{cls.__qualname__}{params}"
|
||||
setattr(mod, f"{cls.__name__}{params}", Unrolled)
|
||||
|
||||
return None
|
||||
|
||||
return decorator
|
||||
@@ -276,6 +276,6 @@ def launch(*launch_args: str) -> None:
|
||||
|
||||
Utils.init_logging("BizHawkClient", exception_logger="Client")
|
||||
import colorama
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -261,6 +261,6 @@ def launch():
|
||||
# options = Utils.get_options()
|
||||
|
||||
import colorama
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -206,7 +206,7 @@ ahit_locations = {
|
||||
"Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"),
|
||||
"Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"),
|
||||
"Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"),
|
||||
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1),
|
||||
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Behind Boss Firewall"),
|
||||
"Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"),
|
||||
"Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"),
|
||||
|
||||
@@ -233,7 +233,7 @@ ahit_locations = {
|
||||
"Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area",
|
||||
required_hats=[HatType.DWELLER], paintings=2),
|
||||
|
||||
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"),
|
||||
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Boss Arena"),
|
||||
|
||||
"Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area",
|
||||
hit_type=HitType.dweller_bell, paintings=1),
|
||||
@@ -411,7 +411,7 @@ act_completions = {
|
||||
"Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
|
||||
required_hats=[HatType.SPRINT]),
|
||||
|
||||
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired",
|
||||
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired - Post Fight",
|
||||
hit_type=HitType.umbrella),
|
||||
|
||||
"Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True),
|
||||
@@ -976,7 +976,6 @@ event_locs = {
|
||||
**snatcher_coins,
|
||||
"HUMT Access": LocData(0, "Heating Up Mafia Town"),
|
||||
"TOD Access": LocData(0, "Toilet of Doom"),
|
||||
"YCHE Access": LocData(0, "Your Contract has Expired"),
|
||||
"AFR Access": LocData(0, "Alpine Free Roam"),
|
||||
"TIHS Access": LocData(0, "The Illness has Spread"),
|
||||
|
||||
|
||||
+24
-1
@@ -347,7 +347,7 @@ def create_regions(world: "HatInTimeWorld"):
|
||||
sf_act3 = create_region_and_connect(world, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest)
|
||||
sf_act4 = create_region_and_connect(world, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest)
|
||||
sf_act5 = create_region_and_connect(world, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest)
|
||||
create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
|
||||
sf_finale = create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
|
||||
|
||||
# ------------------------------------------- ALPINE SKYLINE ------------------------------------------ #
|
||||
alpine_skyline = create_region_and_connect(world, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship)
|
||||
@@ -386,11 +386,24 @@ def create_regions(world: "HatInTimeWorld"):
|
||||
create_rift_connections(world, create_region(world, "Time Rift - Bazaar"))
|
||||
|
||||
sf_area: Region = create_region(world, "Subcon Forest Area")
|
||||
sf_behind_boss_firewall: Region = create_region(world, "Subcon Forest Behind Boss Firewall")
|
||||
sf_boss_arena: Region = create_region(world, "Subcon Forest Boss Arena")
|
||||
sf_area.connect(sf_behind_boss_firewall, "SF Area -> SF Behind Boss Firewall")
|
||||
sf_behind_boss_firewall.connect(sf_boss_arena, "SF Behind Boss Firewall -> SF Boss Arena")
|
||||
sf_act1.connect(sf_area, "Subcon Forest Entrance CO")
|
||||
sf_act2.connect(sf_area, "Subcon Forest Entrance SW")
|
||||
sf_act3.connect(sf_area, "Subcon Forest Entrance TOD")
|
||||
sf_act4.connect(sf_area, "Subcon Forest Entrance QVM")
|
||||
sf_act5.connect(sf_area, "Subcon Forest Entrance MDS")
|
||||
# YCHE puts the player directly in the boss arena, with no access to the rest of Subcon Forest by default.
|
||||
sf_finale.connect(sf_boss_arena, "Subcon Forest Entrance YCHE")
|
||||
# To support the Snatcher Hover expert logic for Act Completion (Your Contract has Expired), the act completion has
|
||||
# to go in a separate region because the Snatcher Hover gives direct access to the Act Completion, but does not
|
||||
# give access to the act itself.
|
||||
sf_finale_post_fight: Region = create_region(world, "Your Contract has Expired - Post Fight")
|
||||
# This connection must never have any rules placed on it because they will not be inherited when setting up act
|
||||
# connections, only the rules for the entrances to the act and the rules for the Act Completion are inherited.
|
||||
sf_finale.connect(sf_finale_post_fight, "YCHE -> YCHE - Post Fight")
|
||||
|
||||
create_rift_connections(world, create_region(world, "Time Rift - Sleepy Subcon"))
|
||||
create_rift_connections(world, create_region(world, "Time Rift - Pipe"))
|
||||
@@ -947,6 +960,16 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def get_region_shuffled_to(world: "HatInTimeWorld", region: str) -> str:
|
||||
if world.options.ActRandomizer:
|
||||
original_ci: str = chapter_act_info[region]
|
||||
shuffled_ci = world.act_connections[original_ci]
|
||||
return next(act_name for act_name, ci in chapter_act_info.items()
|
||||
if ci == shuffled_ci)
|
||||
else:
|
||||
return region
|
||||
|
||||
|
||||
def get_region_location_count(world: "HatInTimeWorld", region_name: str, included_only: bool = True) -> int:
|
||||
count = 0
|
||||
region = world.multiworld.get_region(region_name, world.player)
|
||||
|
||||
+57
-18
@@ -481,9 +481,8 @@ def set_hard_rules(world: "HatInTimeWorld"):
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
|
||||
lambda state: has_paintings(state, world, 3))
|
||||
|
||||
# Cherry bridge over boss arena gap (painting still expected)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
|
||||
# Cherry bridge over boss arena gap
|
||||
set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"), lambda state: True)
|
||||
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
|
||||
lambda state: has_paintings(state, world, 2, True))
|
||||
@@ -566,27 +565,61 @@ def set_expert_rules(world: "HatInTimeWorld"):
|
||||
lambda state: True)
|
||||
|
||||
# Expert: Cherry Hovering
|
||||
subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player)
|
||||
yche = world.multiworld.get_region("Your Contract has Expired", world.player)
|
||||
entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE")
|
||||
# Skipping the boss firewall is possible with a Cherry Hover.
|
||||
set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"),
|
||||
lambda state: has_paintings(state, world, 1, True))
|
||||
# The boss arena gap can be crossed in reverse with a Cherry Hover.
|
||||
subcon_boss_arena = world.get_region("Subcon Forest Boss Arena")
|
||||
subcon_behind_boss_firewall = world.get_region("Subcon Forest Behind Boss Firewall")
|
||||
subcon_boss_arena.connect(subcon_behind_boss_firewall, "SF Boss Arena -> SF Behind Boss Firewall")
|
||||
|
||||
if world.options.NoPaintingSkips:
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 1))
|
||||
subcon_area = world.get_region("Subcon Forest Area")
|
||||
|
||||
# The boss firewall can be skipped in reverse with a Cherry Hover, but it is not possible to remove the boss
|
||||
# firewall from reverse because the paintings to burn to remove the firewall are on the other side of the firewall.
|
||||
# Therefore, a painting skip is required. The paintings could be burned by already having access to
|
||||
# "Subcon Forest Area" through another entrance, but making a new connection to "Subcon Forest Area" in that case
|
||||
# would be pointless.
|
||||
if not world.options.NoPaintingSkips:
|
||||
# The import cannot be done at the module-level because it would cause a circular import.
|
||||
from .Regions import get_region_shuffled_to
|
||||
|
||||
subcon_behind_boss_firewall.connect(subcon_area, "SF Behind Boss Firewall -> SF Area")
|
||||
|
||||
# Because the Your Contract has Expired entrance can now reach "Subcon Forest Area", it needs to be connected to
|
||||
# each of the Subcon Forest Time Rift entrances, like the other Subcon Forest Acts.
|
||||
yche = world.get_region("Your Contract has Expired")
|
||||
|
||||
def connect_to_shuffled_act_at(original_act_name):
|
||||
region_name = get_region_shuffled_to(world, original_act_name)
|
||||
return yche.connect(world.get_region(region_name), f"{original_act_name} Portal - Entrance YCHE")
|
||||
|
||||
# Rules copied from `Rules.set_rift_rules()` with painting logic removed because painting skips must be
|
||||
# available.
|
||||
entrance = connect_to_shuffled_act_at("Time Rift - Pipe")
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
|
||||
reg_act_connection(world, world.get_entrance("Subcon Forest - Act 2").connected_region, entrance)
|
||||
|
||||
entrance = connect_to_shuffled_act_at("Time Rift - Village")
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
|
||||
reg_act_connection(world, world.get_entrance("Subcon Forest - Act 4").connected_region, entrance)
|
||||
|
||||
entrance = connect_to_shuffled_act_at("Time Rift - Sleepy Subcon")
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
|
||||
|
||||
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, True))
|
||||
|
||||
# Set painting rules only. Skipping paintings is determined in has_paintings
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player),
|
||||
lambda state: has_paintings(state, world, 3, True))
|
||||
|
||||
# You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
|
||||
subcon_area.connect(yche, "Snatcher Hover")
|
||||
set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player),
|
||||
lambda state: True)
|
||||
yche_post_fight = world.get_region("Your Contract has Expired - Post Fight")
|
||||
subcon_area.connect(yche_post_fight, "Snatcher Hover")
|
||||
# Cherry Hover from YCHE also works, so there are no requirements for the Act Completion.
|
||||
set_rule(world.get_location("Act Completion (Your Contract has Expired)"), lambda state: True)
|
||||
|
||||
if world.is_dlc2():
|
||||
# Expert: clear Rush Hour with nothing
|
||||
@@ -681,12 +714,18 @@ def set_subcon_rules(world: "HatInTimeWorld"):
|
||||
lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
# You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
|
||||
and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
|
||||
# You can't skip over the boss arena wall without cherry hover.
|
||||
set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"),
|
||||
lambda state: has_paintings(state, world, 1, False))
|
||||
|
||||
# The painting wall can't be skipped without cherry hover, which is Expert
|
||||
# The hookpoints to cross the boss arena gap are only present in Toilet of Doom.
|
||||
set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"),
|
||||
lambda state: state.has("TOD Access", world.player)
|
||||
and can_use_hookshot(state, world))
|
||||
|
||||
# The Act Completion is in the Toilet of Doom region, so the same rules as passing the boss firewall and crossing
|
||||
# the boss arena gap are required. "TOD Access" is implied from the region so does not need to be included in the
|
||||
# rule.
|
||||
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, False))
|
||||
|
||||
+11
-1
@@ -515,10 +515,15 @@ def _populate_sprite_table():
|
||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||
sprite_paths = [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]
|
||||
for dir in [dir for dir in sprite_paths if os.path.isdir(dir)]:
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
if "link" not in _sprite_table:
|
||||
logging.info("Link sprite was not loaded. Loading link from base rom")
|
||||
load_sprite_from_file(get_base_rom_path())
|
||||
|
||||
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
@@ -554,6 +559,11 @@ class Sprite():
|
||||
self.sprite = filedata[0x80000:0x87000]
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
h = hashlib.md5()
|
||||
h.update(filedata)
|
||||
if h.hexdigest() == LTTPJPN10HASH:
|
||||
self.name = "Link"
|
||||
self.author_name = "Nintendo"
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
|
||||
+37
-36
@@ -1120,28 +1120,28 @@ def toss_junk_item(world, player):
|
||||
raise Exception("Unable to find a junk item to toss to make room for a TR small key")
|
||||
|
||||
|
||||
def set_trock_key_rules(world, player):
|
||||
def set_trock_key_rules(multiworld, player):
|
||||
# First set all relevant locked doors to impassible.
|
||||
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
|
||||
set_rule(world.get_entrance(entrance, player), lambda state: False)
|
||||
set_rule(multiworld.get_entrance(entrance, player), lambda state: False)
|
||||
|
||||
all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True)
|
||||
all_state = multiworld.get_all_state(use_cache=False, allow_partial_entrances=True)
|
||||
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
|
||||
all_state.stale[player] = True
|
||||
|
||||
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
|
||||
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player))
|
||||
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player))
|
||||
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player))
|
||||
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player))
|
||||
can_reach_back = all_state.can_reach(multiworld.get_region('Turtle Rock (Eye Bridge)', player))
|
||||
can_reach_front = all_state.can_reach(multiworld.get_region('Turtle Rock (Entrance)', player))
|
||||
can_reach_big_chest = all_state.can_reach(multiworld.get_region('Turtle Rock (Big Chest)', player))
|
||||
can_reach_middle = all_state.can_reach(multiworld.get_region('Turtle Rock (Second Section)', player))
|
||||
|
||||
# If you can't enter from the back, the door to the front of TR requires only 2 small keys if the big key is in one of these chests since 2 key doors are locked behind the big key door.
|
||||
# If you can only enter from the middle, this includes all locations that can only be reached by exiting the front. This can include Laser Bridge and Crystaroller if the front and back connect via Dark DM Ledge!
|
||||
front_locked_locations = {('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)}
|
||||
if can_reach_middle and not can_reach_back and not can_reach_front:
|
||||
normal_regions = all_state.reachable_regions[player].copy()
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
|
||||
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
|
||||
all_state.update_reachable_regions(player)
|
||||
front_locked_regions = all_state.reachable_regions[player].difference(normal_regions)
|
||||
front_locked_locations = set((location.name, player) for region in front_locked_regions for location in region.locations)
|
||||
@@ -1151,37 +1151,38 @@ def set_trock_key_rules(world, player):
|
||||
|
||||
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
|
||||
# otherwise crystaroller room might not be properly marked as reachable through the back.
|
||||
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player))
|
||||
|
||||
|
||||
# No matter what, the key requirement for going from the middle to the bottom should be five keys.
|
||||
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
|
||||
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
|
||||
# might open all the locked doors in any order, so we need maximally restrictive rules.
|
||||
if can_reach_back:
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
else:
|
||||
# Middle to front requires 3 keys if the back is locked by this door, otherwise 5
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
|
||||
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations.union({('Turtle Rock - Pokey 1 Key Drop', player)}))
|
||||
else state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
|
||||
# Middle to front requires 4 keys if the back is locked by this door, otherwise 6
|
||||
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
|
||||
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
|
||||
else state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
|
||||
|
||||
# Front to middle requires 3 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
|
||||
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
|
||||
def tr_big_key_chest_keys_needed(state):
|
||||
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
|
||||
@@ -1194,30 +1195,30 @@ def set_trock_key_rules(world, player):
|
||||
return 6
|
||||
|
||||
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
|
||||
if not can_reach_front and not world.small_key_shuffle[player]:
|
||||
if not can_reach_front and not multiworld.small_key_shuffle[player]:
|
||||
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
|
||||
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
|
||||
if not can_reach_big_chest:
|
||||
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
||||
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
||||
if world.accessibility[player] == 'full':
|
||||
if world.big_key_shuffle[player] and can_reach_big_chest:
|
||||
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
||||
if multiworld.accessibility[player] == 'full':
|
||||
if multiworld.big_key_shuffle[player] and can_reach_big_chest:
|
||||
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
||||
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
||||
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
|
||||
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
|
||||
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
|
||||
forbid_item(multiworld.get_location(location, player), 'Big Key (Turtle Rock)', player)
|
||||
else:
|
||||
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
|
||||
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
|
||||
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
||||
item = item_factory('Small Key (Turtle Rock)', multiworld.worlds[player])
|
||||
location = multiworld.get_location('Turtle Rock - Big Key Chest', player)
|
||||
location.place_locked_item(item)
|
||||
toss_junk_item(world, player)
|
||||
toss_junk_item(multiworld, player)
|
||||
|
||||
if world.accessibility[player] != 'full':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
||||
if multiworld.accessibility[player] != 'full':
|
||||
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
||||
|
||||
|
||||
def set_big_bomb_rules(world, player):
|
||||
|
||||
@@ -79,12 +79,12 @@ class TestInvertedTurtleRock(TestInverted):
|
||||
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
|
||||
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
|
||||
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria']],
|
||||
@@ -97,9 +97,9 @@ class TestInvertedTurtleRock(TestInverted):
|
||||
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
|
||||
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
|
||||
|
||||
@@ -117,12 +117,12 @@ class TestInvertedTurtleRock(TestInverted):
|
||||
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
|
||||
[location, False, [], ['Magic Mirror', 'Lamp']],
|
||||
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
|
||||
# Mirroring into Eye Bridge does not require Cane of Somaria
|
||||
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],
|
||||
|
||||
@@ -80,12 +80,12 @@ class TestInvertedTurtleRock(TestInvertedMinor):
|
||||
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
|
||||
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
|
||||
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria']],
|
||||
@@ -98,9 +98,9 @@ class TestInvertedTurtleRock(TestInvertedMinor):
|
||||
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
|
||||
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
|
||||
])
|
||||
@@ -116,12 +116,12 @@ class TestInvertedTurtleRock(TestInvertedMinor):
|
||||
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
|
||||
[location, False, [], ['Magic Mirror', 'Lamp']],
|
||||
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
|
||||
|
||||
# Mirroring into Eye Bridge does not require Cane of Somaria
|
||||
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],
|
||||
|
||||
@@ -102,7 +102,7 @@ class TestDungeons(TestInvertedOWG):
|
||||
["Turtle Rock - Chain Chomps", True, ['Progressive Sword', 'Progressive Sword', 'Pegasus Boots']],
|
||||
|
||||
["Turtle Rock - Crystaroller Room", False, []],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Big Key (Turtle Rock)', 'Bomb Upgrade (50)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Lamp', 'Cane of Somaria']],
|
||||
|
||||
["Ganons Tower - Hope Room - Left", False, []],
|
||||
|
||||
@@ -120,8 +120,8 @@ class TestDungeons(TestVanillaOWG):
|
||||
#todo: does clip require sword?
|
||||
#["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)', 'Progressive Sword']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)', 'Hookshot']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Big Key (Turtle Rock)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Big Key (Turtle Rock)', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Ganons Tower - Hope Room - Left", False, []],
|
||||
["Ganons Tower - Hope Room - Left", False, ['Moon Pearl', 'Crystal 1']],
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
# Celeste 64 - Changelog
|
||||
|
||||
|
||||
## v1.3
|
||||
|
||||
### Features:
|
||||
|
||||
- New optional Location Checks
|
||||
- Checkpointsanity
|
||||
- Hair Color
|
||||
- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
|
||||
- Other Player Ghosts
|
||||
- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld
|
||||
|
||||
### Quality of Life:
|
||||
|
||||
- Checkpoint Warping
|
||||
- Received Checkpoint items allow for warping to their respective checkpoint
|
||||
- These items are on their respective checkpoint location if Checkpointsanity is disabled
|
||||
- Logic accounts for being able to warp to otherwise inaccessible areas
|
||||
- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
|
||||
- New Options toggle to enable/disable background input
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
- Traffic Blocks now correctly appear disabled within Cassettes
|
||||
|
||||
|
||||
## v1.2
|
||||
|
||||
### Features:
|
||||
|
||||
@@ -39,6 +39,22 @@ move_item_data_table: Dict[str, Celeste64ItemData] = {
|
||||
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
|
||||
}
|
||||
|
||||
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table, **unlockable_item_data_table, **move_item_data_table}
|
||||
checkpoint_item_data_table: Dict[str, Celeste64ItemData] = {
|
||||
ItemName.checkpoint_1: Celeste64ItemData(celeste_64_base_id + 0x20, ItemClassification.progression),
|
||||
ItemName.checkpoint_2: Celeste64ItemData(celeste_64_base_id + 0x21, ItemClassification.progression),
|
||||
ItemName.checkpoint_3: Celeste64ItemData(celeste_64_base_id + 0x22, ItemClassification.progression),
|
||||
ItemName.checkpoint_4: Celeste64ItemData(celeste_64_base_id + 0x23, ItemClassification.progression),
|
||||
ItemName.checkpoint_5: Celeste64ItemData(celeste_64_base_id + 0x24, ItemClassification.progression),
|
||||
ItemName.checkpoint_6: Celeste64ItemData(celeste_64_base_id + 0x25, ItemClassification.progression),
|
||||
ItemName.checkpoint_7: Celeste64ItemData(celeste_64_base_id + 0x26, ItemClassification.progression),
|
||||
ItemName.checkpoint_8: Celeste64ItemData(celeste_64_base_id + 0x27, ItemClassification.progression),
|
||||
ItemName.checkpoint_9: Celeste64ItemData(celeste_64_base_id + 0x28, ItemClassification.progression),
|
||||
ItemName.checkpoint_10: Celeste64ItemData(celeste_64_base_id + 0x29, ItemClassification.progression),
|
||||
}
|
||||
|
||||
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table,
|
||||
**unlockable_item_data_table,
|
||||
**move_item_data_table,
|
||||
**checkpoint_item_data_table}
|
||||
|
||||
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Location
|
||||
from .Names import LocationName
|
||||
from .Names import LocationName, RegionName
|
||||
|
||||
|
||||
celeste_64_base_id: int = 0xCA0000
|
||||
@@ -17,66 +17,80 @@ class Celeste64LocationData(NamedTuple):
|
||||
|
||||
|
||||
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
|
||||
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
|
||||
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
|
||||
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
|
||||
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
|
||||
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
|
||||
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
|
||||
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
|
||||
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
|
||||
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
|
||||
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
|
||||
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
|
||||
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
|
||||
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
|
||||
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
|
||||
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
|
||||
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
|
||||
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
|
||||
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
|
||||
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
|
||||
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
|
||||
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
|
||||
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
|
||||
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
|
||||
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
|
||||
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
|
||||
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
|
||||
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
|
||||
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
|
||||
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
|
||||
LocationName.strawberry_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x00),
|
||||
LocationName.strawberry_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x01),
|
||||
LocationName.strawberry_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x02),
|
||||
LocationName.strawberry_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x03),
|
||||
LocationName.strawberry_5: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x04),
|
||||
LocationName.strawberry_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x05),
|
||||
LocationName.strawberry_7: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x06),
|
||||
LocationName.strawberry_8: Celeste64LocationData(RegionName.nw_girders_island, celeste_64_base_id + 0x07),
|
||||
LocationName.strawberry_9: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x08),
|
||||
LocationName.strawberry_10: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x09),
|
||||
LocationName.strawberry_11: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x0A),
|
||||
LocationName.strawberry_12: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x0B),
|
||||
LocationName.strawberry_13: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x0C),
|
||||
LocationName.strawberry_14: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0D),
|
||||
LocationName.strawberry_15: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0E),
|
||||
LocationName.strawberry_16: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0F),
|
||||
LocationName.strawberry_17: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x10),
|
||||
LocationName.strawberry_18: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x11),
|
||||
LocationName.strawberry_19: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x12),
|
||||
LocationName.strawberry_20: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x13),
|
||||
LocationName.strawberry_21: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x14),
|
||||
LocationName.strawberry_22: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x15),
|
||||
LocationName.strawberry_23: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x16),
|
||||
LocationName.strawberry_24: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x17),
|
||||
LocationName.strawberry_25: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x18),
|
||||
LocationName.strawberry_26: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x19),
|
||||
LocationName.strawberry_27: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1A),
|
||||
LocationName.strawberry_28: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1B),
|
||||
LocationName.strawberry_29: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x1C),
|
||||
LocationName.strawberry_30: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x1D),
|
||||
}
|
||||
|
||||
friend_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
|
||||
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
|
||||
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
|
||||
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
|
||||
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
|
||||
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
|
||||
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
|
||||
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
|
||||
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
|
||||
LocationName.granny_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x00),
|
||||
LocationName.granny_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x01),
|
||||
LocationName.granny_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x02),
|
||||
LocationName.theo_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x03),
|
||||
LocationName.theo_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x04),
|
||||
LocationName.theo_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x05),
|
||||
LocationName.badeline_1: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x06),
|
||||
LocationName.badeline_2: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x07),
|
||||
LocationName.badeline_3: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x08),
|
||||
}
|
||||
|
||||
sign_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
|
||||
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
|
||||
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
|
||||
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
|
||||
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
|
||||
LocationName.sign_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x00),
|
||||
LocationName.sign_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x01),
|
||||
LocationName.sign_3: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x200 + 0x02),
|
||||
LocationName.sign_4: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x200 + 0x03),
|
||||
LocationName.sign_5: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x200 + 0x04),
|
||||
}
|
||||
|
||||
car_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
|
||||
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
|
||||
LocationName.car_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x300 + 0x00),
|
||||
LocationName.car_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x300 + 0x01),
|
||||
}
|
||||
|
||||
checkpoint_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.checkpoint_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x400 + 0x00),
|
||||
LocationName.checkpoint_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x01),
|
||||
LocationName.checkpoint_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x02),
|
||||
LocationName.checkpoint_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x03),
|
||||
LocationName.checkpoint_5: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x04),
|
||||
LocationName.checkpoint_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x05),
|
||||
LocationName.checkpoint_7: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x400 + 0x06),
|
||||
LocationName.checkpoint_8: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x400 + 0x07),
|
||||
LocationName.checkpoint_9: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x400 + 0x08),
|
||||
LocationName.checkpoint_10: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x400 + 0x09),
|
||||
}
|
||||
|
||||
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
|
||||
**friend_location_data_table,
|
||||
**sign_location_data_table,
|
||||
**car_location_data_table}
|
||||
**car_location_data_table,
|
||||
**checkpoint_location_data_table}
|
||||
|
||||
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
|
||||
|
||||
@@ -15,3 +15,18 @@ ground_dash = "Ground Dash"
|
||||
air_dash = "Air Dash"
|
||||
skid_jump = "Skid Jump"
|
||||
climb = "Climb"
|
||||
|
||||
# Checkpoint Items
|
||||
checkpoint_1 = "Intro Checkpoint"
|
||||
checkpoint_2 = "Granny Checkpoint"
|
||||
checkpoint_3 = "South-East Tower Checkpoint"
|
||||
checkpoint_4 = "Climb Sign Checkpoint"
|
||||
checkpoint_5 = "Freeway Checkpoint"
|
||||
checkpoint_6 = "Freeway Feather Checkpoint"
|
||||
checkpoint_7 = "Feather Maze Checkpoint"
|
||||
checkpoint_8 = "Double Dash House Checkpoint"
|
||||
checkpoint_9 = "Badeline Tower Checkpoint"
|
||||
checkpoint_10 = "Badeline Island Checkpoint"
|
||||
|
||||
# Item used for logic definitions that are not possible with the given options
|
||||
cannot_access = "CANNOT ACCESS"
|
||||
|
||||
@@ -10,7 +10,7 @@ strawberry_8 = "Traffic Block Strawberry"
|
||||
strawberry_9 = "South-West Dash Refills Strawberry"
|
||||
strawberry_10 = "South-East Tower Side Strawberry"
|
||||
strawberry_11 = "Girders Strawberry"
|
||||
strawberry_12 = "North-East Tower Bottom Strawberry"
|
||||
strawberry_12 = "Badeline Tower Bottom Strawberry"
|
||||
strawberry_13 = "Breakable Blocks Strawberry"
|
||||
strawberry_14 = "Feather Maze Strawberry"
|
||||
strawberry_15 = "Feather Chain Strawberry"
|
||||
@@ -18,7 +18,7 @@ strawberry_16 = "Feather Hidden Strawberry"
|
||||
strawberry_17 = "Double Dash Puzzle Strawberry"
|
||||
strawberry_18 = "Double Dash Spike Climb Strawberry"
|
||||
strawberry_19 = "Double Dash Spring Strawberry"
|
||||
strawberry_20 = "North-East Tower Breakable Bottom Strawberry"
|
||||
strawberry_20 = "Badeline Tower Breakable Bottom Strawberry"
|
||||
strawberry_21 = "Theo Tower Lower Cassette Strawberry"
|
||||
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
|
||||
strawberry_23 = "South End of Bridge Cassette Strawberry"
|
||||
@@ -27,8 +27,8 @@ strawberry_25 = "Cassette Hidden in the House Strawberry"
|
||||
strawberry_26 = "North End of Bridge Cassette Strawberry"
|
||||
strawberry_27 = "Distant Feather Cassette Strawberry"
|
||||
strawberry_28 = "Feather Arches Cassette Strawberry"
|
||||
strawberry_29 = "North-East Tower Cassette Strawberry"
|
||||
strawberry_30 = "Badeline Cassette Strawberry"
|
||||
strawberry_29 = "Badeline Tower Cassette Strawberry"
|
||||
strawberry_30 = "Badeline Island Cassette Strawberry"
|
||||
|
||||
# Friend Locations
|
||||
granny_1 = "Granny Conversation 1"
|
||||
@@ -51,3 +51,15 @@ sign_5 = "Credits Sign"
|
||||
# Car Locations
|
||||
car_1 = "Intro Car"
|
||||
car_2 = "Secret Car"
|
||||
|
||||
# Checkpoint Locations
|
||||
checkpoint_1 = "Intro Checkpoint"
|
||||
checkpoint_2 = "Granny Checkpoint"
|
||||
checkpoint_3 = "South-East Tower Checkpoint"
|
||||
checkpoint_4 = "Climb Sign Checkpoint"
|
||||
checkpoint_5 = "Freeway Checkpoint"
|
||||
checkpoint_6 = "Freeway Feather Checkpoint"
|
||||
checkpoint_7 = "Feather Maze Checkpoint"
|
||||
checkpoint_8 = "Double Dash House Checkpoint"
|
||||
checkpoint_9 = "Badeline Tower Checkpoint"
|
||||
checkpoint_10 = "Badeline Island Checkpoint"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Level Base Regions
|
||||
forsaken_city = "Forsaken City"
|
||||
|
||||
# Forsaken City Regions
|
||||
intro_islands = "Intro Islands"
|
||||
granny_island = "Granny Island"
|
||||
highway_island = "Freeway Island"
|
||||
nw_girders_island = "North-West Girders Island"
|
||||
ne_feathers_island = "North-East Feathers Island"
|
||||
se_house_island = "South-East House Island"
|
||||
badeline_tower_lower = "Badeline Tower Lower"
|
||||
badeline_tower_upper = "Badeline Tower Upper"
|
||||
badeline_island = "Badeline Island"
|
||||
+159
-2
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
|
||||
from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
|
||||
from Options import Choice, TextChoice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions, OptionError
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
class DeathLinkAmnesty(Range):
|
||||
@@ -18,7 +20,7 @@ class TotalStrawberries(Range):
|
||||
"""
|
||||
display_name = "Total Strawberries"
|
||||
range_start = 0
|
||||
range_end = 46
|
||||
range_end = 55
|
||||
default = 20
|
||||
|
||||
class StrawberriesRequiredPercentage(Range):
|
||||
@@ -73,6 +75,93 @@ class Carsanity(Toggle):
|
||||
"""
|
||||
display_name = "Carsanity"
|
||||
|
||||
class Checkpointsanity(Toggle):
|
||||
"""
|
||||
Whether activating Checkpoints grants location checks
|
||||
|
||||
Activating this will also shuffle items into the pool which allow usage and warping to each Checkpoint
|
||||
"""
|
||||
display_name = "Checkpointsanity"
|
||||
|
||||
|
||||
class ColorChoice(TextChoice):
|
||||
option_strawberry = 0xDB2C00
|
||||
option_empty = 0x6EC0FF
|
||||
option_double = 0xFA91FF
|
||||
option_golden = 0xF2D450
|
||||
option_baddy = 0x9B3FB5
|
||||
option_fire_red = 0xFF0000
|
||||
option_maroon = 0x800000
|
||||
option_salmon = 0xFF3A65
|
||||
option_orange = 0xD86E0A
|
||||
option_lime_green = 0x8DF920
|
||||
option_bright_green = 0x0DAF05
|
||||
option_forest_green = 0x132818
|
||||
option_royal_blue = 0x0036BF
|
||||
option_brown = 0xB78726
|
||||
option_black = 0x000000
|
||||
option_white = 0xFFFFFF
|
||||
option_grey = 0x808080
|
||||
option_any_color = -1
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(cls.option_any_color)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
class MadelineOneDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has one dash
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline One Dash Hair Color"
|
||||
default = ColorChoice.option_strawberry
|
||||
|
||||
class MadelineTwoDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has two dashes
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline Two Dash Hair Color"
|
||||
default = ColorChoice.option_double
|
||||
|
||||
class MadelineNoDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has no dashes
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline No Dash Hair Color"
|
||||
default = ColorChoice.option_empty
|
||||
|
||||
class MadelineFeatherHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has a feather
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline Feather Hair Color"
|
||||
default = ColorChoice.option_golden
|
||||
|
||||
|
||||
class BadelineChaserSource(Choice):
|
||||
"""
|
||||
@@ -119,6 +208,13 @@ celeste_64_option_groups = [
|
||||
Friendsanity,
|
||||
Signsanity,
|
||||
Carsanity,
|
||||
Checkpointsanity,
|
||||
]),
|
||||
OptionGroup("Aesthetic Options", [
|
||||
MadelineOneDashHairColor,
|
||||
MadelineTwoDashHairColor,
|
||||
MadelineNoDashHairColor,
|
||||
MadelineFeatherHairColor,
|
||||
]),
|
||||
OptionGroup("Badeline Chasers", [
|
||||
BadelineChaserSource,
|
||||
@@ -142,7 +238,68 @@ class Celeste64Options(PerGameCommonOptions):
|
||||
friendsanity: Friendsanity
|
||||
signsanity: Signsanity
|
||||
carsanity: Carsanity
|
||||
checkpointsanity: Checkpointsanity
|
||||
|
||||
madeline_one_dash_hair_color: MadelineOneDashHairColor
|
||||
madeline_two_dash_hair_color: MadelineTwoDashHairColor
|
||||
madeline_no_dash_hair_color: MadelineNoDashHairColor
|
||||
madeline_feather_hair_color: MadelineFeatherHairColor
|
||||
|
||||
badeline_chaser_source: BadelineChaserSource
|
||||
badeline_chaser_frequency: BadelineChaserFrequency
|
||||
badeline_chaser_speed: BadelineChaserSpeed
|
||||
|
||||
|
||||
def resolve_options(world: World):
|
||||
# One Dash Hair
|
||||
if isinstance(world.options.madeline_one_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_one_dash_hair_color = int(world.options.madeline_one_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_one_dash_hair_color`:"
|
||||
f"{world.options.madeline_one_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_one_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_one_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_one_dash_hair_color = world.options.madeline_one_dash_hair_color.value
|
||||
|
||||
# Two Dash Hair
|
||||
if isinstance(world.options.madeline_two_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_two_dash_hair_color = int(world.options.madeline_two_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_two_dash_hair_color`:"
|
||||
f"{world.options.madeline_two_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_two_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_two_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_two_dash_hair_color = world.options.madeline_two_dash_hair_color.value
|
||||
|
||||
# No Dash Hair
|
||||
if isinstance(world.options.madeline_no_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_no_dash_hair_color = int(world.options.madeline_no_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_no_dash_hair_color`:"
|
||||
f"{world.options.madeline_no_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_no_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_no_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_no_dash_hair_color = world.options.madeline_no_dash_hair_color.value
|
||||
|
||||
# Feather Hair
|
||||
if isinstance(world.options.madeline_feather_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_feather_hair_color = int(world.options.madeline_feather_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_feather_hair_color`:"
|
||||
f"{world.options.madeline_feather_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_feather_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_feather_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_feather_hair_color = world.options.madeline_feather_hair_color.value
|
||||
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
from typing import Dict, List, NamedTuple
|
||||
|
||||
from .Names import RegionName
|
||||
|
||||
class Celeste64RegionData(NamedTuple):
|
||||
connecting_regions: List[str] = []
|
||||
|
||||
|
||||
region_data_table: Dict[str, Celeste64RegionData] = {
|
||||
"Menu": Celeste64RegionData(["Forsaken City"]),
|
||||
"Forsaken City": Celeste64RegionData(),
|
||||
"Menu": Celeste64RegionData([RegionName.forsaken_city]),
|
||||
|
||||
RegionName.forsaken_city: Celeste64RegionData([RegionName.intro_islands, RegionName.granny_island, RegionName.highway_island, RegionName.ne_feathers_island, RegionName.se_house_island, RegionName.badeline_tower_upper, RegionName.badeline_island]),
|
||||
|
||||
RegionName.intro_islands: Celeste64RegionData([RegionName.granny_island]),
|
||||
RegionName.granny_island: Celeste64RegionData([RegionName.highway_island, RegionName.nw_girders_island, RegionName.badeline_tower_lower, RegionName.se_house_island]),
|
||||
RegionName.highway_island: Celeste64RegionData([RegionName.granny_island, RegionName.ne_feathers_island, RegionName.nw_girders_island]),
|
||||
RegionName.nw_girders_island: Celeste64RegionData([RegionName.highway_island]),
|
||||
RegionName.ne_feathers_island: Celeste64RegionData([RegionName.se_house_island, RegionName.highway_island, RegionName.badeline_tower_lower, RegionName.badeline_tower_upper]),
|
||||
RegionName.se_house_island: Celeste64RegionData([RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_lower]),
|
||||
RegionName.badeline_tower_lower: Celeste64RegionData([RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_upper]),
|
||||
RegionName.badeline_tower_upper: Celeste64RegionData([RegionName.badeline_island, RegionName.badeline_tower_lower, RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island]),
|
||||
RegionName.badeline_island: Celeste64RegionData([RegionName.badeline_tower_upper, RegionName.granny_island, RegionName.highway_island]),
|
||||
}
|
||||
|
||||
+127
-239
@@ -1,265 +1,85 @@
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Tuple, Callable
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, Region
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
from . import Celeste64World
|
||||
from .Names import ItemName, LocationName
|
||||
from .Names import ItemName, LocationName, RegionName
|
||||
|
||||
|
||||
def set_rules(world: Celeste64World):
|
||||
if world.options.logic_difficulty == "standard":
|
||||
if world.options.move_shuffle:
|
||||
world.active_logic_mapping = location_standard_moves_logic
|
||||
else:
|
||||
world.active_logic_mapping = location_standard_logic
|
||||
world.active_logic_mapping = location_standard_moves_logic
|
||||
world.active_region_logic_mapping = region_standard_moves_logic
|
||||
else:
|
||||
if world.options.move_shuffle:
|
||||
world.active_logic_mapping = location_hard_moves_logic
|
||||
else:
|
||||
world.active_logic_mapping = location_hard_logic
|
||||
world.active_logic_mapping = location_hard_moves_logic
|
||||
world.active_region_logic_mapping = region_hard_moves_logic
|
||||
|
||||
for location in world.multiworld.get_locations(world.player):
|
||||
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
|
||||
|
||||
if world.options.logic_difficulty == "standard":
|
||||
if world.options.move_shuffle:
|
||||
world.goal_logic_mapping = goal_standard_moves_logic
|
||||
else:
|
||||
world.goal_logic_mapping = goal_standard_logic
|
||||
else:
|
||||
if world.options.move_shuffle:
|
||||
world.goal_logic_mapping = goal_hard_moves_logic
|
||||
else:
|
||||
world.goal_logic_mapping = goal_hard_logic
|
||||
|
||||
# Completion condition.
|
||||
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
|
||||
|
||||
|
||||
goal_standard_logic: List[List[str]] = [[ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.double_dash_refill]]
|
||||
goal_hard_logic: List[List[str]] = [[]]
|
||||
goal_standard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]]
|
||||
goal_hard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]]
|
||||
|
||||
|
||||
location_standard_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_6: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.strawberry_7: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.strawberry_8: [[ItemName.traffic_block]],
|
||||
LocationName.strawberry_9: [[ItemName.dash_refill]],
|
||||
LocationName.strawberry_11: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables],
|
||||
[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
|
||||
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring]],
|
||||
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.breakables]],
|
||||
|
||||
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.coin]],
|
||||
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill],
|
||||
[ItemName.cassette, ItemName.traffic_block]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables]],
|
||||
|
||||
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
|
||||
LocationName.sign_2: [[ItemName.breakables]],
|
||||
LocationName.sign_3: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables]],
|
||||
}
|
||||
|
||||
location_hard_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_13: [[ItemName.breakables]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
|
||||
LocationName.strawberry_20: [[ItemName.breakables]],
|
||||
|
||||
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_22: [[ItemName.cassette]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin]],
|
||||
LocationName.strawberry_24: [[ItemName.cassette]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables]],
|
||||
|
||||
LocationName.sign_2: [[ItemName.breakables]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables]],
|
||||
}
|
||||
|
||||
location_standard_moves_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_1: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.strawberry_2: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.strawberry_2: [[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_5: [[ItemName.air_dash]],
|
||||
LocationName.strawberry_6: [[ItemName.dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_7: [[ItemName.dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_8: [[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_9: [[ItemName.dash_refill, ItemName.air_dash]],
|
||||
LocationName.strawberry_10: [[ItemName.climb]],
|
||||
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
|
||||
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
|
||||
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
|
||||
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring, ItemName.air_dash]],
|
||||
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_11: [[ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_13: [[ItemName.breakables, ItemName.air_dash],
|
||||
[ItemName.breakables, ItemName.ground_dash]],
|
||||
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash]],
|
||||
LocationName.strawberry_15: [[ItemName.feather, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_16: [[ItemName.feather]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
|
||||
LocationName.strawberry_18: [[ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_19: [[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash, ItemName.skid_jump]],
|
||||
LocationName.strawberry_20: [[ItemName.feather, ItemName.breakables, ItemName.air_dash]],
|
||||
|
||||
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.granny_1: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.granny_2: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.granny_3: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.sign_1: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.sign_3: [[ItemName.dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
|
||||
[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
|
||||
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash, ItemName.climb],
|
||||
[ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
}
|
||||
|
||||
location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_5: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash]],
|
||||
LocationName.strawberry_8: [[ItemName.traffic_block],
|
||||
[ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_10: [[ItemName.air_dash],
|
||||
[ItemName.climb]],
|
||||
LocationName.strawberry_11: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_12: [[ItemName.feather],
|
||||
[ItemName.ground_dash],
|
||||
[ItemName.air_dash]],
|
||||
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.air_dash, ItemName.climb]],
|
||||
[ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.double_dash_refill, ItemName.air_dash]],
|
||||
LocationName.strawberry_15: [[ItemName.feather],
|
||||
[ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
|
||||
@@ -287,42 +107,94 @@ location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
||||
[ItemName.cassette, ItemName.feather, ItemName.climb]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.feather, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.ground_dash, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.feather, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
}
|
||||
|
||||
|
||||
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
|
||||
region_standard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
|
||||
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
|
||||
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
|
||||
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
|
||||
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
|
||||
|
||||
(RegionName.intro_islands, RegionName.granny_island): [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
|
||||
(RegionName.granny_island, RegionName.highway_island): [[ItemName.air_dash, ItemName.dash_refill]],
|
||||
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
|
||||
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.climb, ItemName.dash_refill]],
|
||||
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill]],
|
||||
|
||||
(RegionName.highway_island, RegionName.granny_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.dash_refill]],
|
||||
(RegionName.highway_island, RegionName.ne_feathers_island): [[ItemName.feather]],
|
||||
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.cannot_access]],
|
||||
|
||||
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block]],
|
||||
|
||||
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.climb, ItemName.air_dash, ItemName.feather]],
|
||||
|
||||
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.air_dash, ItemName.traffic_block, ItemName.double_dash_refill]],
|
||||
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.double_dash_refill]],
|
||||
|
||||
(RegionName.badeline_tower_lower, RegionName.se_house_island): [[ItemName.cannot_access]],
|
||||
(RegionName.badeline_tower_lower, RegionName.ne_feathers_island): [[ItemName.air_dash, ItemName.breakables, ItemName.feather]],
|
||||
(RegionName.badeline_tower_lower, RegionName.granny_island): [[ItemName.cannot_access]],
|
||||
(RegionName.badeline_tower_lower, RegionName.badeline_tower_upper): [[ItemName.cannot_access]],
|
||||
|
||||
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
(RegionName.badeline_tower_upper, RegionName.se_house_island): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.badeline_tower_upper, RegionName.ne_feathers_island): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.badeline_tower_upper, RegionName.granny_island): [[ItemName.dash_refill]],
|
||||
|
||||
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
}
|
||||
|
||||
region_hard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
|
||||
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
|
||||
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
|
||||
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
|
||||
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
|
||||
|
||||
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
|
||||
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.double_dash_refill], [ItemName.ground_dash]],
|
||||
|
||||
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.air_dash, ItemName.ground_dash]],
|
||||
|
||||
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.ground_dash]],
|
||||
|
||||
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash], [ItemName.skid_jump]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.feather]],
|
||||
|
||||
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.traffic_block]],
|
||||
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
|
||||
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.traffic_block],
|
||||
[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.skid_jump],
|
||||
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.traffic_block],
|
||||
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.skid_jump]],
|
||||
|
||||
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
}
|
||||
|
||||
|
||||
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
|
||||
if loc not in world.active_logic_mapping:
|
||||
return True
|
||||
|
||||
@@ -332,12 +204,28 @@ def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bo
|
||||
|
||||
return False
|
||||
|
||||
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
|
||||
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
|
||||
return False
|
||||
def region_connection_rule(state: CollectionState, world: Celeste64World, region_connection: Tuple[str]) -> bool:
|
||||
if region_connection not in world.active_region_logic_mapping:
|
||||
return True
|
||||
|
||||
for possible_access in world.goal_logic_mapping:
|
||||
for possible_access in world.active_region_logic_mapping[region_connection]:
|
||||
if state.has_all(possible_access, world.player):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
|
||||
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
|
||||
return False
|
||||
|
||||
goal_region: Region = world.multiworld.get_region(RegionName.badeline_island, world.player)
|
||||
return state.can_reach(goal_region)
|
||||
|
||||
def connect_region(world: Celeste64World, region: Region, dest_regions: List[str]):
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = {}
|
||||
|
||||
for dest_region in dest_regions:
|
||||
region_connection: Tuple[str] = (region.name, dest_region)
|
||||
rules[dest_region] = lambda state, region_connection=region_connection: region_connection_rule(state, world, region_connection)
|
||||
|
||||
region.add_exits(dest_regions, rules)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from BaseClasses import ItemClassification, Location, Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table, item_table
|
||||
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table,\
|
||||
checkpoint_item_data_table, item_table
|
||||
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
|
||||
sign_location_data_table, car_location_data_table, location_table
|
||||
sign_location_data_table, car_location_data_table, checkpoint_location_data_table,\
|
||||
location_table
|
||||
from .Names import ItemName, LocationName
|
||||
from .Options import Celeste64Options, celeste_64_option_groups
|
||||
from .Options import Celeste64Options, celeste_64_option_groups, resolve_options
|
||||
|
||||
|
||||
class Celeste64WebWorld(WebWorld):
|
||||
@@ -42,8 +44,15 @@ class Celeste64World(World):
|
||||
# Instance Data
|
||||
strawberries_required: int
|
||||
active_logic_mapping: Dict[str, List[List[str]]]
|
||||
goal_logic_mapping: Dict[str, List[List[str]]]
|
||||
active_region_logic_mapping: Dict[Tuple[str], List[List[str]]]
|
||||
|
||||
madeline_one_dash_hair_color: int
|
||||
madeline_two_dash_hair_color: int
|
||||
madeline_no_dash_hair_color: int
|
||||
madeline_feather_hair_color: int
|
||||
|
||||
def generate_early(self) -> None:
|
||||
resolve_options(self)
|
||||
|
||||
def create_item(self, name: str) -> Celeste64Item:
|
||||
# Only make required amount of strawberries be Progression
|
||||
@@ -76,25 +85,49 @@ class Celeste64World(World):
|
||||
for name in unlockable_item_data_table.keys()
|
||||
if name not in self.options.start_inventory]
|
||||
|
||||
if self.options.move_shuffle:
|
||||
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
|
||||
chosen_start_item: str = ""
|
||||
|
||||
if self.options.move_shuffle:
|
||||
if self.options.logic_difficulty == "standard":
|
||||
# If the start_inventory already includes a move, don't worry about giving it one
|
||||
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
|
||||
chosen_start_move = self.random.choice(move_items_for_itempool)
|
||||
move_items_for_itempool.remove(chosen_start_move)
|
||||
possible_unwalls: List[str] = [name for name in move_item_data_table.keys()
|
||||
if name != ItemName.skid_jump]
|
||||
|
||||
if self.options.checkpointsanity:
|
||||
possible_unwalls.extend([name for name in checkpoint_item_data_table.keys()
|
||||
if name != ItemName.checkpoint_1 and name != ItemName.checkpoint_10])
|
||||
|
||||
# If the start_inventory already includes a move or checkpoint, don't worry about giving it one
|
||||
if not [item for item in possible_unwalls if item in self.multiworld.precollected_items[self.player]]:
|
||||
chosen_start_item = self.random.choice(possible_unwalls)
|
||||
|
||||
if self.options.carsanity:
|
||||
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
|
||||
intro_car_loc.place_locked_item(self.create_item(chosen_start_move))
|
||||
intro_car_loc.place_locked_item(self.create_item(chosen_start_item))
|
||||
location_count -= 1
|
||||
else:
|
||||
self.multiworld.push_precollected(self.create_item(chosen_start_move))
|
||||
self.multiworld.push_precollected(self.create_item(chosen_start_item))
|
||||
|
||||
item_pool += [self.create_item(name)
|
||||
for name in move_items_for_itempool
|
||||
if name not in self.options.start_inventory]
|
||||
for name in move_item_data_table.keys()
|
||||
if name not in self.multiworld.precollected_items[self.player]
|
||||
and name != chosen_start_item]
|
||||
else:
|
||||
for start_move in move_item_data_table.keys():
|
||||
self.multiworld.push_precollected(self.create_item(start_move))
|
||||
|
||||
if self.options.checkpointsanity:
|
||||
location_count += 9
|
||||
goal_checkpoint_loc: Location = self.multiworld.get_location(LocationName.checkpoint_10, self.player)
|
||||
goal_checkpoint_loc.place_locked_item(self.create_item(ItemName.checkpoint_10))
|
||||
item_pool += [self.create_item(name)
|
||||
for name in checkpoint_item_data_table.keys()
|
||||
if name not in self.multiworld.precollected_items[self.player]
|
||||
and name != ItemName.checkpoint_10
|
||||
and name != chosen_start_item]
|
||||
else:
|
||||
for item_name in checkpoint_item_data_table.keys():
|
||||
checkpoint_loc: Location = self.multiworld.get_location(item_name, self.player)
|
||||
checkpoint_loc.place_locked_item(self.create_item(item_name))
|
||||
|
||||
real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - len(item_pool))
|
||||
self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
|
||||
@@ -140,18 +173,23 @@ class Celeste64World(World):
|
||||
if location_data.region == region_name
|
||||
}, Celeste64Location)
|
||||
|
||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||
region.add_locations({
|
||||
location_name: location_data.address for location_name, location_data in checkpoint_location_data_table.items()
|
||||
if location_data.region == region_name
|
||||
}, Celeste64Location)
|
||||
|
||||
from .Rules import connect_region
|
||||
connect_region(self, region, region_data_table[region_name].connecting_regions)
|
||||
|
||||
# Have to do this here because of other games using State in a way that's bad
|
||||
from .Rules import set_rules
|
||||
set_rules(self)
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return ItemName.raspberry
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
from .Rules import set_rules
|
||||
set_rules(self)
|
||||
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"death_link": self.options.death_link.value,
|
||||
@@ -161,6 +199,11 @@ class Celeste64World(World):
|
||||
"friendsanity": self.options.friendsanity.value,
|
||||
"signsanity": self.options.signsanity.value,
|
||||
"carsanity": self.options.carsanity.value,
|
||||
"checkpointsanity": self.options.checkpointsanity.value,
|
||||
"madeline_one_dash_hair_color": self.madeline_one_dash_hair_color,
|
||||
"madeline_two_dash_hair_color": self.madeline_two_dash_hair_color,
|
||||
"madeline_no_dash_hair_color": self.madeline_no_dash_hair_color,
|
||||
"madeline_feather_hair_color": self.madeline_feather_hair_color,
|
||||
"badeline_chaser_source": self.options.badeline_chaser_source.value,
|
||||
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
|
||||
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import io
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||
import zipfile
|
||||
from BaseClasses import Location
|
||||
from worlds.Files import APContainer
|
||||
from worlds.Files import APContainer, AutoPatchRegister
|
||||
|
||||
from .Enum import CivVICheckType
|
||||
from .Locations import CivVILocation, CivVILocationData
|
||||
@@ -25,18 +26,22 @@ class CivTreeItem:
|
||||
ui_tree_row: int
|
||||
|
||||
|
||||
class CivVIContainer(APContainer):
|
||||
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
"""
|
||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||
"""
|
||||
game: Optional[str] = "Civilization VI"
|
||||
patch_file_ending = ".apcivvi"
|
||||
|
||||
def __init__(self, patch_data: Dict[str, str], base_path: str, output_directory: str,
|
||||
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "",
|
||||
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
if isinstance(patch_data, io.BytesIO):
|
||||
super().__init__(patch_data, player, player_name, server)
|
||||
else:
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
for filename, yml in self.patch_data.items():
|
||||
|
||||
@@ -19,8 +19,8 @@ from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
||||
get_start_inventory_data
|
||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
|
||||
CVCOTM_VC_US_HASH
|
||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH
|
||||
# CVCOTM_VC_US_HASH
|
||||
from .client import CastlevaniaCotMClient
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ class CVCotMSettings(settings.Group):
|
||||
"""File name of the Castlevania CotM US rom"""
|
||||
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
||||
description = "Castlevania CotM (US) ROM File"
|
||||
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
# md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
@@ -153,11 +153,10 @@ Advance Collection ROM; most notably the fact that the audio does not function w
|
||||
which is currently a requirement to connect to a multiworld. This happens because all audio code was stripped
|
||||
from the ROM, and all sound is instead played by the collection through external means.
|
||||
|
||||
For this reason, it is most recommended to obtain the ROM by dumping it from an original cartridge of the game that you legally own.
|
||||
Though, the Advance Collection *can* still technically be an option if you cannot do that and don't mind the lack of sound.
|
||||
The Wii U Virtual Console version does not work due to changes in the code in that version.
|
||||
|
||||
The Wii U Virtual Console version is currently untested. If you happen to have purchased it before the Wii U eShop shut down, you can try
|
||||
dumping and playing with it. However, at the moment, we cannot guarantee that it will work well due to it being untested.
|
||||
Due to the reasons mentioned above, it is most recommended to obtain the ROM by dumping it from an original cartridge of the
|
||||
game that you legally own. However, the Advance Collection *is* an option if you cannot do that and don't mind the lack of sound.
|
||||
|
||||
Regardless of which released ROM you intend to try playing with, the US version of the game is required.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
|
||||
- A Castlevania: Circle of the Moon ROM of the US version specifically. The Archipelago community cannot provide this.
|
||||
The Castlevania Advance Collection ROM can technically be used, but it has no audio. The Wii U Virtual Console ROM is untested.
|
||||
The Castlevania Advance Collection ROM can be used, but it has no audio. The Wii U Virtual Console ROM does not work.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later.
|
||||
|
||||
### Configuring BizHawk
|
||||
|
||||
@@ -22,11 +22,9 @@ if TYPE_CHECKING:
|
||||
|
||||
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
|
||||
CVCOTM_AC_US_HASH = "87a1bd6577b6702f97a60fc55772ad74" # Castlevania Advance Collection ROM
|
||||
CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
|
||||
# CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
|
||||
|
||||
# NOTE: The Wii U VC version is untested as of when this comment was written. I am only including its hash in case it
|
||||
# does work. If someone who has it can confirm it does indeed work, this comment should be removed. If it doesn't, the
|
||||
# hash should be removed in addition. See the Game Page for more information about supported versions.
|
||||
# The Wii U VC version is not currently supported. See the Game Page for more info.
|
||||
|
||||
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
|
||||
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
|
||||
@@ -518,7 +516,8 @@ class CVCotMPatchExtensions(APPatchExtension):
|
||||
|
||||
|
||||
class CVCotMProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
# hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
|
||||
patch_file_ending: str = ".apcvcotm"
|
||||
result_file_ending: str = ".gba"
|
||||
|
||||
@@ -585,7 +584,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
|
||||
# if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
|
||||
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]:
|
||||
raise Exception("Supplied Base ROM does not match known MD5s for Castlevania: Circle of the Moon USA."
|
||||
"Get the correct game and version, then dump it.")
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||
|
||||
@@ -530,7 +530,7 @@ server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
||||
def launch():
|
||||
import colorama
|
||||
global executable, server_settings, server_args
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
if server_settings:
|
||||
server_settings = os.path.abspath(server_settings)
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
|
||||
## Installing the Archipelago Mod using Lumafly
|
||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||
2. Click the "Install" button near the "Archipelago" mod entry.
|
||||
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
|
||||
2. Install the Archipelago mods by doing either of the following:
|
||||
* Click one of the links below to allow Lumafly to install the mods. Lumafly will prompt for confirmation.
|
||||
* [Archipelago and dependencies only](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago)
|
||||
* [Archipelago with rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/)
|
||||
(includes Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn,
|
||||
and AdditionalMaps).
|
||||
* Click the "Install" button near the "Archipelago" mod entry. If desired, also install "Archipelago Map Mod"
|
||||
to use as an in-game tracker.
|
||||
3. Launch the game, you're all set!
|
||||
|
||||
### What to do if Lumafly fails to find your installation directory
|
||||
|
||||
@@ -295,6 +295,6 @@ def launch():
|
||||
parser = get_base_parser(description="KH1 Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -981,6 +981,6 @@ def launch():
|
||||
parser = get_base_parser(description="KH2 Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -107,6 +107,7 @@ SYNONYMS = {
|
||||
'JUMP': 'FEATHER',
|
||||
'PLUME': 'FEATHER',
|
||||
'WING': 'FEATHER',
|
||||
"QUILL": "FEATHER",
|
||||
|
||||
# SHOVEL
|
||||
'DIG': 'SHOVEL',
|
||||
@@ -343,6 +344,8 @@ SYNONYMS = {
|
||||
# TRADING_ITEM_LETTER
|
||||
'CARD': 'TRADING_ITEM_LETTER',
|
||||
'MESSAGE': 'TRADING_ITEM_LETTER',
|
||||
"TICKET": 'TRADING_ITEM_LETTER',
|
||||
"PASS": 'TRADING_ITEM_LETTER',
|
||||
|
||||
# TRADING_ITEM_BROOM
|
||||
'SWEEP': 'TRADING_ITEM_BROOM',
|
||||
@@ -365,6 +368,8 @@ SYNONYMS = {
|
||||
'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||
'SCOPE': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||
'XRAY': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||
"DETECTOR": 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||
"ITEMFINDER": 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||
|
||||
# PIECE_OF_POWER
|
||||
'TRIANGLE': 'PIECE_OF_POWER',
|
||||
@@ -378,6 +383,7 @@ PHRASES = {
|
||||
'BOSS KEY': 'NIGHTMARE_KEY',
|
||||
'HEART PIECE': 'HEART_PIECE',
|
||||
'PIECE OF HEART': 'HEART_PIECE',
|
||||
"ROCK SMASH": 'BOMB',
|
||||
}
|
||||
|
||||
# All following will only be used to match items for the specific game.
|
||||
@@ -404,6 +410,16 @@ GAME_SPECIFIC_PHRASES = {
|
||||
|
||||
'Ocarina of Time': {
|
||||
'COJIRO': 'ROOSTER',
|
||||
"Goron Tunic": "RED_TUNIC",
|
||||
"Zora Tunic": "BLUE_TUNIC",
|
||||
"Wallet": "MAGIC_POWDER",
|
||||
"Medallion": "PIECE_OF_POWER",
|
||||
"Kokiri Emerald": "RUPEES_500",
|
||||
"Goron Ruby": "RUPEES_500",
|
||||
"Zora Sapphire": "RUPEES_500",
|
||||
"Dins Fire": "MAGIC_ROD", # Fire shield
|
||||
"Nayrus Love": "MAGIC_ROD", # Protective barrier
|
||||
"Farores Wind": "MAGIC_ROD", # Create/use warp point in dungeons
|
||||
},
|
||||
|
||||
'SMZ3': {
|
||||
@@ -417,10 +433,14 @@ GAME_SPECIFIC_PHRASES = {
|
||||
|
||||
'Sonic Adventure 2 Battle': {
|
||||
'CHAOS EMERALD': 'PIECE_OF_POWER',
|
||||
"Rings": "RUPEES_20", # This should only affect filler Rings currency, not Flame Ring upgrade
|
||||
"Grapes": "TRADING_ITEM_PINEAPPLE",
|
||||
"Pick Nails": "SHOVEL", # Digging upgrade
|
||||
},
|
||||
|
||||
'Super Mario 64': {
|
||||
'POWER STAR': 'PIECE_OF_POWER',
|
||||
"Key": "NIGHTMARE_KEY" # Affect 2nd Floor / Basement / Progressive keys
|
||||
},
|
||||
|
||||
'Super Mario World': {
|
||||
@@ -528,4 +548,336 @@ GAME_SPECIFIC_PHRASES = {
|
||||
'2500 Tokens': 'RUPEES_500',
|
||||
'5000 Tokens': 'RUPEES_500',
|
||||
},
|
||||
|
||||
"Donkey Kong Country 3": {
|
||||
"Flupperius Petallus Pongus": "TRADING_ITEM_HIBISCUS", # It's a flower in the game
|
||||
"Banana Bird": "ROOSTER", # Made sure this is a BIRD, not a BANANA
|
||||
},
|
||||
|
||||
"Pokemon Red and Blue": {
|
||||
|
||||
# Key Items
|
||||
|
||||
"Old Amber": "STONE_BEAK", # Aerodactyl's fossil should still be a fossil
|
||||
"Coin Case": "MAGIC_POWDER", # This shouldn't spawn as RUPEES
|
||||
"Bike Voucher": "TRADING_ITEM_LETTER",
|
||||
"Oak's Parcel": "TRADING_ITEM_LETTER",
|
||||
|
||||
# Drinks always get converted to MEDICINE
|
||||
"Soda Pop": "MEDICINE",
|
||||
"Fresh Water": "MEDICINE",
|
||||
|
||||
# Consumables
|
||||
"Elixir": "MEDICINE",
|
||||
"Ether": "MEDICINE",
|
||||
"Antidote": "MEDICINE",
|
||||
"Awakening": "MEDICINE",
|
||||
"Burn Heal": "MEDICINE",
|
||||
"Ice Heal": "MEDICINE",
|
||||
"Paralyze Heal": "MEDICINE",
|
||||
"Full Heal": "MEDICINE",
|
||||
"Full Restore": "MEDICINE",
|
||||
},
|
||||
|
||||
"Pokemon Emerald": {
|
||||
|
||||
"Coin Case": "MAGIC_POWDER", # This shouldn't spawn as RUPEES
|
||||
|
||||
# Drinks always get converted to MEDICINE
|
||||
|
||||
"Soda Pop": "MEDICINE",
|
||||
"Fresh Water": "MEDICINE",
|
||||
|
||||
# Consumables
|
||||
"Elixir": "MEDICINE",
|
||||
"Ether": "MEDICINE",
|
||||
"Antidote": "MEDICINE",
|
||||
"Awakening": "MEDICINE",
|
||||
"Burn Heal": "MEDICINE",
|
||||
"Ice Heal": "MEDICINE",
|
||||
"Paralyze Heal": "MEDICINE",
|
||||
"Full Heal": "MEDICINE",
|
||||
"Full Restore": "MEDICINE",
|
||||
"Nanab Berry": "TRADING_ITEM_BANANAS", # Special exception for Nanab Berry, which look like bananas
|
||||
"Berry": "TRADING_ITEM_PINEAPPLE",
|
||||
"Mail": "TRADING_ITEM_LETTER", # Snail mail, not chain mail
|
||||
},
|
||||
|
||||
"Mario & Luigi Superstar Saga": {
|
||||
|
||||
# Key Items
|
||||
"Peach's Extra Dress": "RED_TUNIC",
|
||||
"Peasley's Rose": "TRADING_ITEM_HIBISCUS",
|
||||
"Beanstar": "PIECE_OF_POWER", # Hits both Fake Beanstar and pieces of the real Beanstar, hopefully
|
||||
"Beanstone": "RUPEES_500", # They're gemstones
|
||||
"Firebrand": "POWER_BRACELET", # Magic power that affects Mario/Luigi's hands, either this or MAGIC_ROD would be okay
|
||||
"Thunderhand": "POWER_BRACELET", # Ditto
|
||||
|
||||
# 1-UP Super fix
|
||||
"1-UP Super": "TOADSTOOL",
|
||||
|
||||
# Drinks --> medicine
|
||||
|
||||
# Syrup bottles
|
||||
"Syrup": "MEDICINE",
|
||||
|
||||
# Coffee blends
|
||||
"Hoolumbian": "MEDICINE",
|
||||
"Chuckoccino": "MEDICINE",
|
||||
"Teeheespresso": "MEDICINE",
|
||||
"Blend": "MEDICINE", # for all coffee blends
|
||||
|
||||
# Secret Scrolls --> MESSAGE
|
||||
"Secret Scroll": "TRADING_ITEM_LETTER",
|
||||
|
||||
# Goblets --> MEDICINE
|
||||
"Goblet": "MEDICINE",
|
||||
|
||||
# Pearl Beans --> Fruit
|
||||
"Pearl Bean": 'TRADING_ITEM_PINEAPPLE',
|
||||
|
||||
# Bros. Armor --> Blue Tunic
|
||||
"Pants": "BLUE_TUNIC",
|
||||
"Jeans": "BLUE_TUNIC",
|
||||
"Trousers": "BLUE_TUNIC",
|
||||
"Slacks": "BLUE_TUNIC",
|
||||
"Casual Coral": "BLUE_TUNIC",
|
||||
"Shroom Bells": "BLUE_TUNIC",
|
||||
|
||||
# Badges --> Ribbon
|
||||
"Badge": "TRADING_ITEM_RIBBON",
|
||||
"Soulful Bros.": "TRADING_ITEM_RIBBON",
|
||||
"Bros. Rock": "TRADING_ITEM_RIBBON",
|
||||
|
||||
# Misc. Beans --> Acorns
|
||||
"Hoo Bean": "GUARDIAN_ACORN", # Beans and nuts are similar enough, right?
|
||||
"Chuckle Bean": "GUARDIAN_ACORN",
|
||||
"Hee Bean": "GUARDIAN_ACORN",
|
||||
"Woo Bean": "GUARDIAN_ACORN",
|
||||
},
|
||||
|
||||
"DOOM 1993": {
|
||||
"Keycard": "KEY",
|
||||
"Computer area map": "MAP",
|
||||
"Box of": "SINGLE_ARROW", # bullets, rockets, or shotgun shells
|
||||
"Energy cell pack": "SINGLE_ARROW",
|
||||
"Chainsaw": "SWORD",
|
||||
"Medikit": "MEDICINE",
|
||||
"Skull key": "NIGHTMARE_KEY",
|
||||
},
|
||||
|
||||
"DOOM II": {
|
||||
"Keycard": "KEY",
|
||||
"Computer area map": "MAP",
|
||||
"Box of": "SINGLE_ARROW", # bullets, rockets, or shotgun shells
|
||||
"Energy cell pack": "SINGLE_ARROW",
|
||||
"Chainsaw": "SWORD",
|
||||
"Medikit": "MEDICINE",
|
||||
"Skull key": "NIGHTMARE_KEY",
|
||||
},
|
||||
|
||||
"Inscryption": {
|
||||
"Extra Candle": "HEART_CONTAINER", # Candles act as extra health
|
||||
"Magnificus Eye": "TRADING_ITEM_MAGNIFYING_GLASS", # Needed to see hidden drawings / messages
|
||||
"Monocle": "TRADING_ITEM_MAGNIFYING_GLASS", # Ditto
|
||||
"Pile Of Meat": "TRADING_ITEM_DOG_FOOD",
|
||||
"Angler Hook": "TRADING_ITEM_FISHING_HOOK", # Good fish.
|
||||
"Currency": "RUPEES_20",
|
||||
},
|
||||
|
||||
"Minecraft": {
|
||||
"Progressive Weapons": "SWORD",
|
||||
"Progressive Tools": "SHOVEL",
|
||||
"Archery": "BOW",
|
||||
"Emerald": "RUPEES_20",
|
||||
"Brewing": "MEDICINE",
|
||||
"Spyglass": 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||
"Porkchop": "TRADING_ITEM_DOG_FOOD"
|
||||
},
|
||||
"VVVVVV": {
|
||||
"Trinket": "PIECE_OF_POWER",
|
||||
},
|
||||
|
||||
"A Hat in Time": {
|
||||
"Time Piece": "PIECE_OF_POWER",
|
||||
"Metro Ticket": "TRADING_ITEM_LETTER",
|
||||
"Snatcher's Contract": "TRADING_ITEM_LETTER",
|
||||
"Pon": "RUPEES_20",
|
||||
},
|
||||
|
||||
"Kingdom Hearts 2": {
|
||||
# Goal items / Collectibles
|
||||
"Proof of": "PIECE_OF_POWER",
|
||||
"Lucky Emblem": "PIECE_OF_POWER",
|
||||
"Secret Ansem's Report": "TRADING_ITEM_LETTER",
|
||||
|
||||
# Sora Keyblades
|
||||
"Bond of Flame": "SWORD",
|
||||
"Circle of Life": "SWORD",
|
||||
"Decisive Pumpkin": "SWORD",
|
||||
"Fatal Crest": "SWORD",
|
||||
"Fenrir": "SWORD",
|
||||
"Follow the Wind": "SWORD",
|
||||
"Guardian Soul": "SWORD",
|
||||
"Gull Wing": "SWORD",
|
||||
"Hero's Crest": "SWORD",
|
||||
"Hidden Dragon": "SWORD",
|
||||
"Monochrome": "SWORD",
|
||||
"Mysterious Abyss": "SWORD",
|
||||
"Oathkeeper": "SWORD",
|
||||
"Oblivion": "SWORD",
|
||||
"Photon Debugger": "SWORD",
|
||||
"Pureblood": "SWORD",
|
||||
"Rumbling Rose": "SWORD",
|
||||
"Sleeping Lion": "SWORD",
|
||||
"Star Seeker": "SWORD",
|
||||
"Sweet Memories": "SWORD",
|
||||
"Two Become One": "SWORD",
|
||||
"Ultima Weapon": "SWORD",
|
||||
"Winner's Proof": "SWORD",
|
||||
"Wishing Lamp": "SWORD",
|
||||
|
||||
# Donald Staves
|
||||
"Centurion+": "MAGIC_ROD",
|
||||
"Nobody Lance": "MAGIC_ROD",
|
||||
"Precious Mushroom": "MAGIC_ROD",
|
||||
"Precious Mushroom+": "MAGIC_ROD",
|
||||
"Premium Mushroom": "MAGIC_ROD",
|
||||
"Rising Dragon": "MAGIC_ROD",
|
||||
"Save The Queen+": "MAGIC_ROD",
|
||||
"Shaman's Relic": "MAGIC_ROD",
|
||||
"Victory Bell": "MAGIC_ROD",
|
||||
|
||||
# Goofy Shields
|
||||
"Akashic Record": "SHIELD",
|
||||
"Frozen Pride+": "SHIELD",
|
||||
"Majestic Mushroom": "SHIELD",
|
||||
"Majestic Mushroom+": "SHIELD",
|
||||
"Nobody Guard": "SHIELD",
|
||||
"Ogre Shield": "SHIELD",
|
||||
"Save The King+": "SHIELD",
|
||||
"Ultimate Mushroom": "SHIELD",
|
||||
|
||||
# Accessories as RIBBON
|
||||
"Star Charm": "TRADING_ITEM_RIBBON",
|
||||
"Ring": "TRADING_ITEM_RIBBON",
|
||||
"Earring": "TRADING_ITEM_RIBBON",
|
||||
"Shadow Archive": "TRADING_ITEM_RIBBON",
|
||||
"Shadow Archive+": "TRADING_ITEM_RIBBON",
|
||||
"Full Bloom": "TRADING_ITEM_RIBBON",
|
||||
"Full Bloom+": "TRADING_ITEM_RIBBON",
|
||||
|
||||
# Armor as BLUE_TUNIC
|
||||
"Bandanna": "BLUE_TUNIC",
|
||||
"Belt": "BLUE_TUNIC",
|
||||
"Band": "BLUE_TUNIC",
|
||||
"Bangle": "BLUE_TUNIC",
|
||||
"Armlet": "BLUE_TUNIC",
|
||||
"Trinket": "BLUE_TUNIC",
|
||||
"Charm": "BLUE_TUNIC",
|
||||
"Anklet": "BLUE_TUNIC",
|
||||
"Chain": "BLUE_TUNIC",
|
||||
"Acrisius": "BLUE_TUNIC",
|
||||
"Ribbon": "BLUE_TUNIC",
|
||||
|
||||
# Magic
|
||||
"Element": "MAGIC_ROD",
|
||||
|
||||
# Other
|
||||
"Munny Pouch": "MAGIC_POWDER",
|
||||
"Ether": "MEDICINE",
|
||||
"Elixir": "MEDICINE",
|
||||
"Megalixir": "MEDICINE",
|
||||
},
|
||||
|
||||
"Kingdom Hearts": {
|
||||
# Goal/collectible items
|
||||
"Ansem's Report": "TRADING_ITEM_LETTER",
|
||||
|
||||
# Dalmatian puppies
|
||||
"Puppy": "BOWWOW",
|
||||
"Puppies": "BOWWOW",
|
||||
|
||||
# Sora Keyblades
|
||||
"Jungle King": "SWORD",
|
||||
"Three Wishes": "SWORD",
|
||||
"Fairy Harp": "SWORD",
|
||||
"Pumpkinhead": "SWORD",
|
||||
"Crabclaw": "SWORD",
|
||||
"Divine Rose": "SWORD",
|
||||
"Spellbinder": "SWORD",
|
||||
"Olympia": "SWORD",
|
||||
"Lionheart": "SWORD",
|
||||
"Metal Chocobo": "SWORD",
|
||||
"Oathkeeper": "SWORD",
|
||||
"Oblivion": "SWORD",
|
||||
"Lady Luck": "SWORD",
|
||||
"Wishing Star": "SWORD",
|
||||
"Ultima Weapon": "SWORD",
|
||||
"Diamond Dust": "SWORD",
|
||||
"One-Winged Angel": "SWORD",
|
||||
|
||||
# Donald Staves
|
||||
"Morning Star": "MAGIC_ROD",
|
||||
"Shooting Star": "MAGIC_ROD",
|
||||
"Warhammer": "MAGIC_ROD",
|
||||
"Silver Mallet": "MAGIC_ROD",
|
||||
"Grand Mallet": "MAGIC_ROD",
|
||||
"Lord Fortune": "MAGIC_ROD",
|
||||
"Violetta": "MAGIC_ROD",
|
||||
"Save the Queen": "MAGIC_ROD",
|
||||
"Wizard's Relic": "MAGIC_ROD",
|
||||
"Meteor Strike": "MAGIC_ROD",
|
||||
"Fantasista": "MAGIC_ROD",
|
||||
|
||||
# Goofy Shields
|
||||
"Smasher": "SHIELD",
|
||||
"Gigas Fist": "SHIELD",
|
||||
"Save the King": "SHIELD",
|
||||
"Defender": "SHIELD",
|
||||
"Seven Elements": "SHIELD",
|
||||
|
||||
# Magic
|
||||
"Progressive Fire": "MAGIC_ROD",
|
||||
"Progressive Blizzard": "MAGIC_ROD",
|
||||
"Progressive Thunder": "MAGIC_ROD",
|
||||
"Progressive Cure": "MAGIC_ROD",
|
||||
"Progressive Gravity": "MAGIC_ROD",
|
||||
"Progressive Stop": "MAGIC_ROD",
|
||||
"Progressive Aero": "MAGIC_ROD",
|
||||
|
||||
# Accessories / armor (Let's go with BLUE_TUNIC for these, these items are closer to RPG armor anyways)
|
||||
"Chain": "BLUE_TUNIC",
|
||||
"Ring": "BLUE_TUNIC",
|
||||
"Band": "BLUE_TUNIC",
|
||||
"Three Stars": "BLUE_TUNIC",
|
||||
"Stud": "BLUE_TUNIC",
|
||||
"Earring": "BLUE_TUNIC",
|
||||
"Bangle": "BLUE_TUNIC",
|
||||
"Armlet": "BLUE_TUNIC",
|
||||
"Moogle Badge": "BLUE_TUNIC",
|
||||
"Cosmic Arts": "BLUE_TUNIC",
|
||||
"Heartguard": "BLUE_TUNIC",
|
||||
"Crystal Crown": "BLUE_TUNIC",
|
||||
"Ribbon": "BLUE_TUNIC",
|
||||
"Brave Warrior": "BLUE_TUNIC",
|
||||
"Ifrit's Horn": "BLUE_TUNIC",
|
||||
"White Fang": "BLUE_TUNIC",
|
||||
"Ray of Light": "BLUE_TUNIC",
|
||||
"Circlet": "BLUE_TUNIC",
|
||||
"Raven's Claw": "BLUE_TUNIC",
|
||||
"Omega Arts": "BLUE_TUNIC",
|
||||
"Royal Crown": "BLUE_TUNIC",
|
||||
"Prime Cap": "BLUE_TUNIC",
|
||||
"Belt": "BLUE_TUNIC",
|
||||
"EXP Bracelet": "BLUE_TUNIC",
|
||||
"EXP Necklace": "BLUE_TUNIC",
|
||||
|
||||
# Other
|
||||
"Glide": "FEATHER",
|
||||
"Ether": "MEDICINE",
|
||||
"Elixir": "MEDICINE",
|
||||
"Megalixir": "MEDICINE",
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ class LinksAwakeningWorld(World):
|
||||
phrases.update(ItemIconGuessing.GAME_SPECIFIC_PHRASES[foreign_game])
|
||||
|
||||
for phrase, icon in phrases.items():
|
||||
if phrase in uppered:
|
||||
if phrase.upper() in uppered:
|
||||
return icon
|
||||
# pattern for breaking down camelCase, also separates out digits
|
||||
pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=\d)")
|
||||
|
||||
@@ -131,7 +131,7 @@ def generate_random_hints(world: "LandstalkerWorld"):
|
||||
hint_texts = list(set(hint_texts))
|
||||
random.shuffle(hint_texts)
|
||||
|
||||
hint_count = world.options.hint_count.value
|
||||
hint_count = min(world.options.hint_count.value, len(hint_texts))
|
||||
del hint_texts[hint_count:]
|
||||
|
||||
hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if
|
||||
|
||||
@@ -39,7 +39,7 @@ class LandstalkerWorld(World):
|
||||
item_name_to_id = build_item_name_to_id_table()
|
||||
location_name_to_id = build_location_name_to_id_table()
|
||||
|
||||
cached_spheres: List[Set[Location]]
|
||||
cached_spheres: List[Set[Location]] = []
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super().__init__(multiworld, player)
|
||||
@@ -48,9 +48,11 @@ class LandstalkerWorld(World):
|
||||
self.dark_region_ids = []
|
||||
self.teleport_tree_pairs = []
|
||||
self.jewel_items = []
|
||||
self.cached_spheres = []
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
if not LandstalkerWorld.cached_spheres:
|
||||
LandstalkerWorld.cached_spheres = list(self.multiworld.get_spheres())
|
||||
|
||||
# Generate hints.
|
||||
self.adjust_shop_prices()
|
||||
hints = Hints.generate_random_hints(self)
|
||||
@@ -232,18 +234,9 @@ class LandstalkerWorld(World):
|
||||
else:
|
||||
return 4
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, multiworld: MultiWorld):
|
||||
# Cache spheres for hint calculation after fill completes.
|
||||
cached_spheres = list(multiworld.get_spheres())
|
||||
for world in multiworld.get_game_worlds(cls.game):
|
||||
world.cached_spheres = cached_spheres
|
||||
|
||||
@classmethod
|
||||
def stage_modify_multidata(cls, multiworld: MultiWorld, *_):
|
||||
# Clean up all references in cached spheres after generation completes.
|
||||
for world in multiworld.get_game_worlds(cls.game):
|
||||
world.cached_spheres = []
|
||||
LandstalkerWorld.cached_spheres = []
|
||||
|
||||
def adjust_shop_prices(self):
|
||||
# Calculate prices for items in shops once all items have their final position
|
||||
@@ -254,7 +247,7 @@ class LandstalkerWorld(World):
|
||||
|
||||
global_price_factor = self.options.shop_prices_factor / 100.0
|
||||
|
||||
spheres = self.cached_spheres
|
||||
spheres = LandstalkerWorld.cached_spheres
|
||||
sphere_count = len(spheres)
|
||||
for sphere_id, sphere in enumerate(spheres):
|
||||
location: LandstalkerLocation # after conditional, we guarantee it's this kind of location.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- A compatible emulator to run the game
|
||||
- [RetroArch](https://retroarch.com?page=platforms) with the Genesis Plus GX core
|
||||
- [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) with the Genesis Plus GX core
|
||||
- Your legally obtained Landstalker US ROM file (which can be acquired on [Steam](https://store.steampowered.com/app/71118/Landstalker_The_Treasures_of_King_Nole/))
|
||||
- A Landstalker US ROM file dumped from the original cartridge
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, ClassVar, TextIO
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from Options import Accessibility
|
||||
from Utils import output_path
|
||||
from settings import FilePath, Group
|
||||
@@ -17,6 +17,7 @@ from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
|
||||
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
|
||||
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
|
||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
from .transitions import shuffle_transitions
|
||||
|
||||
components.append(
|
||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||
@@ -128,7 +129,7 @@ class MessengerWorld(World):
|
||||
spoiler_portal_mapping: dict[str, str]
|
||||
portal_mapping: list[int]
|
||||
transitions: list[Entrance]
|
||||
reachable_locs: int = 0
|
||||
reachable_locs: bool = False
|
||||
filler: dict[str, int]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
@@ -145,13 +146,13 @@ class MessengerWorld(World):
|
||||
|
||||
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
|
||||
|
||||
starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine", "Searing Crags"]
|
||||
starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine",
|
||||
"Searing Crags"]
|
||||
self.starting_portals = [f"{portal} Portal"
|
||||
for portal in starting_portals[:3] +
|
||||
self.random.sample(starting_portals[3:], k=self.options.available_portals - 3)]
|
||||
|
||||
# super complicated method for adding searing crags to starting portals if it wasn't chosen
|
||||
# TODO add a check for transition shuffle when that gets added back in
|
||||
if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals:
|
||||
self.starting_portals.append("Searing Crags Portal")
|
||||
portals_to_strip = [portal for portal in ["Riviere Turquoise Portal", "Sunken Shrine Portal"]
|
||||
@@ -181,7 +182,7 @@ class MessengerWorld(World):
|
||||
region_name = region.name.removeprefix(f"{region.parent} - ")
|
||||
connection_data = CONNECTIONS[region.parent][region_name]
|
||||
for exit_region in connection_data:
|
||||
region.connect(self.multiworld.get_region(exit_region, self.player))
|
||||
region.connect(self.get_region(exit_region))
|
||||
|
||||
# all regions need to be created before i can do these connections so we create and connect the complex first
|
||||
for region in [level for level in simple_regions if level.name in REGION_CONNECTIONS]:
|
||||
@@ -256,6 +257,7 @@ class MessengerWorld(World):
|
||||
f" {logic} for {self.multiworld.get_player_name(self.player)}")
|
||||
# MessengerOOBRules(self).set_messenger_rules()
|
||||
|
||||
def connect_entrances(self) -> None:
|
||||
add_closed_portal_reqs(self)
|
||||
# i need portal shuffle to happen after rules exist so i can validate it
|
||||
attempts = 5
|
||||
@@ -271,6 +273,9 @@ class MessengerWorld(World):
|
||||
else:
|
||||
raise RuntimeError("Unable to generate valid portal output.")
|
||||
|
||||
if self.options.shuffle_transitions:
|
||||
shuffle_transitions(self)
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||
if self.options.available_portals < 6:
|
||||
spoiler_handle.write(f"\nStarting Portals:\n\n")
|
||||
@@ -286,9 +291,54 @@ class MessengerWorld(World):
|
||||
key=lambda portal:
|
||||
["Autumn Hills", "Riviere Turquoise",
|
||||
"Howling Grotto", "Sunken Shrine",
|
||||
"Searing Crags", "Glacial Peak"].index(portal[0]))
|
||||
"Searing Crags", "Glacial Peak"].index(portal[0])
|
||||
)
|
||||
for portal, output in portal_info:
|
||||
spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
|
||||
spoiler.set_entrance(f"{portal} Portal", output, "", self.player)
|
||||
|
||||
if self.options.shuffle_transitions:
|
||||
for transition in self.transitions:
|
||||
if (transition.randomization_type == EntranceType.TWO_WAY
|
||||
and (transition.connected_region.name, "both", self.player) in spoiler.entrances):
|
||||
continue
|
||||
spoiler.set_entrance(
|
||||
transition.name if "->" not in transition.name else transition.parent_region.name,
|
||||
transition.connected_region.name,
|
||||
"both" if transition.randomization_type == EntranceType.TWO_WAY
|
||||
and self.options.shuffle_transitions == ShuffleTransitions.option_coupled else "",
|
||||
self.player
|
||||
)
|
||||
|
||||
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
|
||||
if not self.options.shuffle_transitions:
|
||||
return
|
||||
|
||||
hint_data.update({self.player: {}})
|
||||
|
||||
all_state = self.multiworld.get_all_state(True)
|
||||
# sometimes some of my regions aren't in path for some reason?
|
||||
all_state.update_reachable_regions(self.player)
|
||||
paths = all_state.path
|
||||
start = self.get_region("Tower HQ")
|
||||
start_connections = [entrance.name for entrance in start.exits if entrance not in {"Home", "Shrink Down"}]
|
||||
transition_names = [transition.name for transition in self.transitions] + start_connections
|
||||
for loc in self.get_locations():
|
||||
if (loc.parent_region.name in {"Tower HQ", "The Shop", "Music Box", "The Craftsman's Corner"}
|
||||
or loc.address is None):
|
||||
continue
|
||||
path_to_loc: list[str] = []
|
||||
name, connection = paths.get(loc.parent_region, (None, None))
|
||||
while connection != ("Menu", None) and name is not None:
|
||||
name, connection = connection
|
||||
if name in transition_names:
|
||||
if name in start_connections:
|
||||
name = f"{name} -> {self.get_entrance(name).connected_region.name}"
|
||||
path_to_loc.append(name)
|
||||
|
||||
text = " => ".join(reversed(path_to_loc))
|
||||
if not text:
|
||||
continue
|
||||
hint_data[self.player][loc.address] = text
|
||||
|
||||
def fill_slot_data(self) -> dict[str, Any]:
|
||||
slot_data = {
|
||||
@@ -308,11 +358,13 @@ class MessengerWorld(World):
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if not getattr(self, "_filler_items", None):
|
||||
self._filler_items = [name for name in self.random.choices(
|
||||
list(self.filler),
|
||||
weights=list(self.filler.values()),
|
||||
k=20
|
||||
)]
|
||||
self._filler_items = [
|
||||
name for name in self.random.choices(
|
||||
list(self.filler),
|
||||
weights=list(self.filler.values()),
|
||||
k=20
|
||||
)
|
||||
]
|
||||
return self._filler_items.pop(0)
|
||||
|
||||
def create_item(self, name: str) -> MessengerItem:
|
||||
@@ -331,7 +383,7 @@ class MessengerWorld(World):
|
||||
self.total_shards += count
|
||||
return ItemClassification.progression_skip_balancing if count else ItemClassification.filler
|
||||
|
||||
if name == "Windmill Shuriken" and getattr(self, "multiworld", None) is not None:
|
||||
if name == "Windmill Shuriken":
|
||||
return ItemClassification.progression if self.options.logic_level else ItemClassification.filler
|
||||
|
||||
if name == "Power Seal":
|
||||
@@ -344,7 +396,7 @@ class MessengerWorld(World):
|
||||
|
||||
if name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
|
||||
return ItemClassification.useful
|
||||
|
||||
|
||||
if name in TRAPS:
|
||||
return ItemClassification.trap
|
||||
|
||||
@@ -354,7 +406,7 @@ class MessengerWorld(World):
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World:
|
||||
group = super().create_group(multiworld, new_player_id, players)
|
||||
assert isinstance(group, MessengerWorld)
|
||||
|
||||
|
||||
group.filler = FILLER.copy()
|
||||
group.options.traps.value = all(multiworld.worlds[player].options.traps for player in players)
|
||||
if group.options.traps:
|
||||
|
||||
@@ -244,14 +244,12 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Bottom Left": [
|
||||
"Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Sand Trap Shop",
|
||||
"Quillshroom Marsh - Bottom Right",
|
||||
],
|
||||
"Top Right": [
|
||||
"Quillshroom Marsh - Queen of Quills Shop",
|
||||
"Searing Crags - Left",
|
||||
],
|
||||
"Bottom Right": [
|
||||
"Quillshroom Marsh - Bottom Left",
|
||||
"Quillshroom Marsh - Sand Trap Shop",
|
||||
"Searing Crags - Bottom",
|
||||
],
|
||||
@@ -639,43 +637,43 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
}
|
||||
|
||||
RANDOMIZED_CONNECTIONS: dict[str, str] = {
|
||||
"Ninja Village - Right": "Autumn Hills - Left",
|
||||
"Autumn Hills - Left": "Ninja Village - Right",
|
||||
"Autumn Hills - Right": "Forlorn Temple - Left",
|
||||
"Autumn Hills - Bottom": "Catacombs - Bottom Left",
|
||||
"Forlorn Temple - Left": "Autumn Hills - Right",
|
||||
"Forlorn Temple - Right": "Bamboo Creek - Top Left",
|
||||
"Forlorn Temple - Bottom": "Catacombs - Top Left",
|
||||
"Catacombs - Top Left": "Forlorn Temple - Bottom",
|
||||
"Catacombs - Bottom Left": "Autumn Hills - Bottom",
|
||||
"Catacombs - Bottom": "Dark Cave - Right",
|
||||
"Catacombs - Right": "Bamboo Creek - Bottom Left",
|
||||
"Bamboo Creek - Bottom Left": "Catacombs - Right",
|
||||
"Bamboo Creek - Right": "Howling Grotto - Left",
|
||||
"Bamboo Creek - Top Left": "Forlorn Temple - Right",
|
||||
"Howling Grotto - Left": "Bamboo Creek - Right",
|
||||
"Howling Grotto - Top": "Quillshroom Marsh - Bottom Left",
|
||||
"Howling Grotto - Right": "Quillshroom Marsh - Top Left",
|
||||
"Howling Grotto - Bottom": "Sunken Shrine - Left",
|
||||
"Quillshroom Marsh - Top Left": "Howling Grotto - Right",
|
||||
"Quillshroom Marsh - Bottom Left": "Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Top Right": "Searing Crags - Left",
|
||||
"Ninja Village - Right": "Autumn Hills - Left",
|
||||
"Autumn Hills - Left": "Ninja Village - Right",
|
||||
"Autumn Hills - Right": "Forlorn Temple - Left",
|
||||
"Autumn Hills - Bottom": "Catacombs - Bottom Left",
|
||||
"Forlorn Temple - Left": "Autumn Hills - Right",
|
||||
"Forlorn Temple - Right": "Bamboo Creek - Top Left",
|
||||
"Forlorn Temple - Bottom": "Catacombs - Top Left",
|
||||
"Catacombs - Top Left": "Forlorn Temple - Bottom",
|
||||
"Catacombs - Bottom Left": "Autumn Hills - Bottom",
|
||||
"Catacombs - Bottom": "Dark Cave - Right",
|
||||
"Catacombs - Right": "Bamboo Creek - Bottom Left",
|
||||
"Bamboo Creek - Bottom Left": "Catacombs - Right",
|
||||
"Bamboo Creek - Right": "Howling Grotto - Left",
|
||||
"Bamboo Creek - Top Left": "Forlorn Temple - Right",
|
||||
"Howling Grotto - Left": "Bamboo Creek - Right",
|
||||
"Howling Grotto - Top": "Quillshroom Marsh - Bottom Left",
|
||||
"Howling Grotto - Right": "Quillshroom Marsh - Top Left",
|
||||
"Howling Grotto - Bottom": "Sunken Shrine - Left",
|
||||
"Quillshroom Marsh - Top Left": "Howling Grotto - Right",
|
||||
"Quillshroom Marsh - Bottom Left": "Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Top Right": "Searing Crags - Left",
|
||||
"Quillshroom Marsh - Bottom Right": "Searing Crags - Bottom",
|
||||
"Searing Crags - Left": "Quillshroom Marsh - Top Right",
|
||||
"Searing Crags - Top": "Glacial Peak - Bottom",
|
||||
"Searing Crags - Bottom": "Quillshroom Marsh - Bottom Right",
|
||||
"Searing Crags - Right": "Underworld - Left",
|
||||
"Glacial Peak - Bottom": "Searing Crags - Top",
|
||||
"Glacial Peak - Top": "Cloud Ruins - Left",
|
||||
"Glacial Peak - Left": "Elemental Skylands - Air Shmup",
|
||||
"Cloud Ruins - Left": "Glacial Peak - Top",
|
||||
"Elemental Skylands - Right": "Glacial Peak - Left",
|
||||
"Tower HQ": "Tower of Time - Left",
|
||||
"Artificer": "Corrupted Future",
|
||||
"Underworld - Left": "Searing Crags - Right",
|
||||
"Dark Cave - Right": "Catacombs - Bottom",
|
||||
"Dark Cave - Left": "Riviere Turquoise - Right",
|
||||
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
||||
"Searing Crags - Left": "Quillshroom Marsh - Top Right",
|
||||
"Searing Crags - Top": "Glacial Peak - Bottom",
|
||||
"Searing Crags - Bottom": "Quillshroom Marsh - Bottom Right",
|
||||
"Searing Crags - Right": "Underworld - Left",
|
||||
"Glacial Peak - Bottom": "Searing Crags - Top",
|
||||
"Glacial Peak - Top": "Cloud Ruins - Left",
|
||||
"Glacial Peak - Left": "Elemental Skylands - Air Shmup",
|
||||
"Cloud Ruins - Left": "Glacial Peak - Top",
|
||||
"Elemental Skylands - Right": "Glacial Peak - Left",
|
||||
"Tower HQ": "Tower of Time - Left",
|
||||
"Artificer": "Corrupted Future",
|
||||
"Underworld - Left": "Searing Crags - Right",
|
||||
"Dark Cave - Right": "Catacombs - Bottom",
|
||||
"Dark Cave - Left": "Riviere Turquoise - Right",
|
||||
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
||||
}
|
||||
|
||||
TRANSITIONS: list[str] = [
|
||||
|
||||
+29
-12
@@ -3,7 +3,8 @@ from dataclasses import dataclass
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle
|
||||
from . import RANDOMIZED_CONNECTIONS
|
||||
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
|
||||
|
||||
|
||||
@@ -27,20 +28,36 @@ class PortalPlando(PlandoConnections):
|
||||
- entrance: Searing Crags
|
||||
exit: Glacial Peak Portal
|
||||
"""
|
||||
display_name = "Portal Plando Connections"
|
||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
||||
checkpoints = [point for points in CHECKPOINTS.values() for point in points]
|
||||
portal_entrances = PORTALS
|
||||
portal_exits = portals + shop_points + checkpoints
|
||||
entrances = portal_entrances
|
||||
exits = portal_exits
|
||||
|
||||
entrances = frozenset(PORTALS)
|
||||
exits = frozenset(portals + shop_points + checkpoints)
|
||||
|
||||
|
||||
# for back compatibility. To later be replaced with transition plando
|
||||
class HiddenPortalPlando(PortalPlando):
|
||||
visibility = Visibility.none
|
||||
entrances = PortalPlando.entrances
|
||||
exits = PortalPlando.exits
|
||||
class TransitionPlando(PlandoConnections):
|
||||
"""
|
||||
Plando connections to be used with transition shuffle.
|
||||
List of valid connections can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L641.
|
||||
Dictionary keys (left) are entrances and values (right) are exits. If transition shuffle is on coupled all plando
|
||||
connections will be coupled. If on decoupled, "entrance" and "exit" will be treated the same, simply making the
|
||||
plando connection one-way from entrance to exit.
|
||||
Example:
|
||||
- entrance: Searing Crags - Top
|
||||
exit: Dark Cave - Right
|
||||
direction: both
|
||||
"""
|
||||
display_name = "Transition Plando Connections"
|
||||
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
||||
exits = frozenset(RANDOMIZED_CONNECTIONS.values())
|
||||
|
||||
@classmethod
|
||||
def can_connect(cls, entrance: str, exit: str) -> bool:
|
||||
if entrance != "Glacial Peak - Left" and entrance.lower() in cls.exits:
|
||||
return exit.lower() in cls.entrances
|
||||
return exit.lower() not in cls.entrances
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -226,7 +243,7 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
early_meditation: EarlyMed
|
||||
available_portals: AvailablePortals
|
||||
shuffle_portals: ShufflePortals
|
||||
# shuffle_transitions: ShuffleTransitions
|
||||
shuffle_transitions: ShuffleTransitions
|
||||
goal: Goal
|
||||
music_box: MusicBox
|
||||
notes_needed: NotesNeeded
|
||||
@@ -236,4 +253,4 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
shop_price: ShopPrices
|
||||
shop_price_plan: PlannedShopPrices
|
||||
portal_plando: PortalPlando
|
||||
plando_connections: HiddenPortalPlando
|
||||
plando_connections: TransitionPlando
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, PlandoOptions
|
||||
from BaseClasses import CollectionState
|
||||
from Options import PlandoConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -252,9 +252,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
||||
world.random.shuffle(available_portals)
|
||||
|
||||
plando = world.options.portal_plando.value
|
||||
if not plando:
|
||||
plando = world.options.plando_connections.value
|
||||
if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals:
|
||||
if plando and not world.plando_portals:
|
||||
try:
|
||||
handle_planned_portals(plando)
|
||||
# any failure i expect will trigger on available_portals.remove
|
||||
@@ -294,8 +292,8 @@ def disconnect_portals(world: "MessengerWorld") -> None:
|
||||
|
||||
|
||||
def validate_portals(world: "MessengerWorld") -> bool:
|
||||
# if world.options.shuffle_transitions:
|
||||
# return True
|
||||
if world.options.shuffle_transitions:
|
||||
return True
|
||||
new_state = CollectionState(world.multiworld)
|
||||
new_state.update_reachable_regions(world.player)
|
||||
reachable_locs = 0
|
||||
|
||||
@@ -32,7 +32,7 @@ class MessengerRules:
|
||||
self.connection_rules = {
|
||||
# from ToTHQ
|
||||
"Artificer's Portal":
|
||||
lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
|
||||
lambda state: state.has("Demon King Crown", self.player),
|
||||
"Shrink Down":
|
||||
lambda state: state.has_all(NOTES, self.player),
|
||||
# the shop
|
||||
@@ -267,6 +267,8 @@ class MessengerRules:
|
||||
# tower of time
|
||||
"Tower of Time Seal - Time Waster":
|
||||
self.has_dart,
|
||||
# corrupted future
|
||||
"Corrupted Future - Key of Courage": lambda state: state.has("Magic Firefly", self.player),
|
||||
# cloud ruins
|
||||
"Time Warp Mega Shard":
|
||||
lambda state: self.has_vertical(state) or self.can_dboost(state),
|
||||
@@ -370,7 +372,7 @@ class MessengerRules:
|
||||
add_rule(multiworld.get_entrance("Shrink Down", self.player), self.has_dart)
|
||||
multiworld.completion_condition[self.player] = lambda state: state.has("Do the Thing!", self.player)
|
||||
if self.world.options.accessibility: # not locations accessibility
|
||||
set_self_locking_items(self.world, self.player)
|
||||
set_self_locking_items(self.world)
|
||||
|
||||
|
||||
class MessengerHardRules(MessengerRules):
|
||||
@@ -530,9 +532,11 @@ class MessengerOOBRules(MessengerRules):
|
||||
self.world.options.accessibility.value = MessengerAccessibility.option_minimal
|
||||
|
||||
|
||||
def set_self_locking_items(world: "MessengerWorld", player: int) -> None:
|
||||
def set_self_locking_items(world: "MessengerWorld") -> None:
|
||||
# locations where these placements are always valid
|
||||
allow_self_locking_items(world.get_location("Searing Crags - Key of Strength").parent_region, "Power Thistle")
|
||||
allow_self_locking_items(world.get_location("Sunken Shrine - Key of Love"), "Sun Crest", "Moon Crest")
|
||||
allow_self_locking_items(world.get_location("Corrupted Future - Key of Courage").parent_region, "Demon King Crown")
|
||||
allow_self_locking_items(world.get_location("Elemental Skylands Seal - Water"), "Currents Master")
|
||||
if not world.options.shuffle_transitions:
|
||||
allow_self_locking_items(world.get_location("Corrupted Future - Key of Courage").parent_region,
|
||||
"Demon King Crown")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
|
||||
from entrance_rando import ERPlacementState
|
||||
from .regions import LOCATIONS, MEGA_SHARDS
|
||||
from .shop import FIGURINES, SHOP_ITEMS
|
||||
|
||||
@@ -12,9 +13,21 @@ if TYPE_CHECKING:
|
||||
class MessengerEntrance(Entrance):
|
||||
world: "MessengerWorld | None" = None
|
||||
|
||||
def can_connect_to(self, other: Entrance, dead_end: bool, state: "ERPlacementState") -> bool:
|
||||
can_connect = super().can_connect_to(other, dead_end, state)
|
||||
world: MessengerWorld = getattr(self, "world", None)
|
||||
if not world or world.reachable_locs or not can_connect:
|
||||
return can_connect
|
||||
empty_state = CollectionState(world.multiworld, True)
|
||||
self.connected_region = other.connected_region
|
||||
empty_state.update_reachable_regions(world.player)
|
||||
world.reachable_locs = any(loc.can_reach(empty_state) and not loc.is_event for loc in world.get_locations())
|
||||
self.connected_region = None
|
||||
return world.reachable_locs and (not state.coupled or self.name != other.name)
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
parent: str
|
||||
parent: str | None
|
||||
entrance_type = MessengerEntrance
|
||||
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
||||
@@ -32,8 +45,9 @@ class MessengerRegion(Region):
|
||||
for shop_loc in SHOP_ITEMS}
|
||||
self.add_locations(shop_locations, MessengerShopLocation)
|
||||
elif name == "The Craftsman's Corner":
|
||||
self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||
MessengerLocation)
|
||||
self.add_locations(
|
||||
{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||
MessengerLocation)
|
||||
elif name == "Tower HQ":
|
||||
locations.append("Money Wrench")
|
||||
|
||||
@@ -57,6 +71,7 @@ class MessengerLocation(Location):
|
||||
|
||||
|
||||
class MessengerShopLocation(MessengerLocation):
|
||||
|
||||
@cached_property
|
||||
def cost(self) -> int:
|
||||
name = self.name.removeprefix("The Shop - ")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
|
||||
from . import MessengerTestBase
|
||||
|
||||
|
||||
class StrictEntranceRandoTest(MessengerTestBase):
|
||||
"""Bare-bones world that tests the strictest possible settings to ensure it doesn't crash"""
|
||||
auto_construct = True
|
||||
options = {
|
||||
"limited_movement": 1,
|
||||
"available_portals": 3,
|
||||
"shuffle_portals": 1,
|
||||
"shuffle_transitions": 1,
|
||||
}
|
||||
|
||||
@unittest.skip
|
||||
def test_all_state_can_reach_everything(self) -> None:
|
||||
"""It's not possible to reach everything with these options so skip this test."""
|
||||
pass
|
||||
@@ -0,0 +1,101 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Region
|
||||
from entrance_rando import EntranceType, randomize_entrances
|
||||
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
|
||||
from .options import ShuffleTransitions, TransitionPlando
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
|
||||
def remove_dangling_exit(region: Region) -> None:
|
||||
# find the disconnected exit and remove references to it
|
||||
for _exit in region.exits:
|
||||
if not _exit.connected_region:
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
|
||||
region.exits.remove(_exit)
|
||||
|
||||
def remove_dangling_entrance(region: Region) -> None:
|
||||
# find the disconnected entrance and remove references to it
|
||||
for _entrance in region.entrances:
|
||||
if not _entrance.parent_region:
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Invalid target region for {plando_connection}")
|
||||
region.entrances.remove(_entrance)
|
||||
|
||||
for plando_connection in plando_connections:
|
||||
# get the connecting regions
|
||||
# need to handle these special because the names are unique but have the same parent region
|
||||
if plando_connection.entrance in ("Artificer", "Tower HQ"):
|
||||
reg1 = world.get_region("Tower HQ")
|
||||
if plando_connection.entrance == "Artificer":
|
||||
dangling_exit = world.get_entrance("Artificer's Portal")
|
||||
else:
|
||||
dangling_exit = world.get_entrance("Artificer's Challenge")
|
||||
reg1.exits.remove(dangling_exit)
|
||||
else:
|
||||
reg1 = world.get_region(plando_connection.entrance)
|
||||
remove_dangling_exit(reg1)
|
||||
|
||||
reg2 = world.get_region(plando_connection.exit)
|
||||
remove_dangling_entrance(reg2)
|
||||
# connect the regions
|
||||
reg1.connect(reg2)
|
||||
|
||||
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
|
||||
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
or plando_connection.direction == "both")
|
||||
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
|
||||
remove_dangling_exit(reg2)
|
||||
remove_dangling_entrance(reg1)
|
||||
reg2.connect(reg1)
|
||||
|
||||
|
||||
def shuffle_transitions(world: "MessengerWorld") -> None:
|
||||
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
|
||||
def disconnect_entrance() -> None:
|
||||
child_region.entrances.remove(entrance)
|
||||
entrance.connected_region = None
|
||||
|
||||
er_type = EntranceType.ONE_WAY if child == "Glacial Peak - Left" else \
|
||||
EntranceType.TWO_WAY if child in RANDOMIZED_CONNECTIONS else EntranceType.ONE_WAY
|
||||
if er_type == EntranceType.TWO_WAY:
|
||||
mock_entrance = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
mock_entrance = child_region.create_er_target(child)
|
||||
|
||||
entrance.randomization_type = er_type
|
||||
mock_entrance.randomization_type = er_type
|
||||
|
||||
for parent, child in RANDOMIZED_CONNECTIONS.items():
|
||||
if child == "Corrupted Future":
|
||||
entrance = world.get_entrance("Artificer's Portal")
|
||||
elif child == "Tower of Time - Left":
|
||||
entrance = world.get_entrance("Artificer's Challenge")
|
||||
else:
|
||||
entrance = world.get_entrance(f"{parent} -> {child}")
|
||||
parent_region = entrance.parent_region
|
||||
child_region = entrance.connected_region
|
||||
entrance.world = world
|
||||
disconnect_entrance()
|
||||
|
||||
plando = world.options.plando_connections
|
||||
if plando:
|
||||
connect_plando(world, plando)
|
||||
|
||||
result = randomize_entrances(world, coupled, {0: [0]})
|
||||
|
||||
world.transitions = sorted(result.placements, key=lambda entrance: TRANSITIONS.index(entrance.parent_region.name))
|
||||
|
||||
for transition in world.transitions:
|
||||
if "->" not in transition.name:
|
||||
continue
|
||||
transition.parent_region.exits.remove(transition)
|
||||
transition.name = f"{transition.parent_region.name} -> {transition.connected_region.name}"
|
||||
transition.parent_region.exits.append(transition)
|
||||
+10
-9
@@ -100,14 +100,15 @@ class OOTWeb(WebWorld):
|
||||
["Edos"]
|
||||
)
|
||||
|
||||
setup_es = Tutorial(
|
||||
setup.tutorial_name,
|
||||
setup.description,
|
||||
"Español",
|
||||
"setup_es.md",
|
||||
"setup/es",
|
||||
setup.authors
|
||||
)
|
||||
# Very out of date, requires updating to match current
|
||||
# setup_es = Tutorial(
|
||||
# setup.tutorial_name,
|
||||
# setup.description,
|
||||
# "Español",
|
||||
# "setup_es.md",
|
||||
# "setup/es",
|
||||
# setup.authors
|
||||
# )
|
||||
|
||||
setup_fr = Tutorial(
|
||||
setup.tutorial_name,
|
||||
@@ -127,7 +128,7 @@ class OOTWeb(WebWorld):
|
||||
["Held_der_Zeit"]
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_es, setup_fr, setup_de]
|
||||
tutorials = [setup, setup_fr, setup_de]
|
||||
option_groups = oot_option_groups
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"can_break_heated_crate": "deadly_bonks != 'ohko' or (Fairy and (can_use(Goron_Tunic) or damage_multiplier != 'ohko')) or can_use(Nayrus_Love) or can_blast_or_smash",
|
||||
"can_break_lower_beehive": "can_use(Boomerang) or can_use(Hookshot) or Bombs or (logic_beehives_bombchus and has_bombchus)",
|
||||
"can_break_upper_beehive": "can_use(Boomerang) or can_use(Hookshot) or (logic_beehives_bombchus and has_bombchus)",
|
||||
"can_break_upper_beehive_child": "can_use(Boomerang) or (logic_beehives_bombchus and has_bombchus and is_child)",
|
||||
# can_use and helpers
|
||||
# The parser reduces this to smallest form based on item category.
|
||||
# Note that can_use(item) is False for any item not covered here.
|
||||
|
||||
@@ -2233,8 +2233,8 @@
|
||||
"ZD Pot 3": "True",
|
||||
"ZD Pot 4": "True",
|
||||
"ZD Pot 5": "True",
|
||||
"ZD In Front of King Zora Beehive 1": "is_child and can_break_upper_beehive",
|
||||
"ZD In Front of King Zora Beehive 2": "is_child and can_break_upper_beehive",
|
||||
"ZD In Front of King Zora Beehive 1": "can_break_upper_beehive_child",
|
||||
"ZD In Front of King Zora Beehive 2": "can_break_upper_beehive_child",
|
||||
"ZD GS Frozen Waterfall": "
|
||||
is_adult and at_night and
|
||||
(Hookshot or Bow or Magic_Meter or logic_domain_gs)",
|
||||
@@ -2259,7 +2259,7 @@
|
||||
"scene": "Zoras Domain",
|
||||
"hint": "ZORAS_DOMAIN",
|
||||
"locations": {
|
||||
"ZD Behind King Zora Beehive": "is_child and can_break_upper_beehive"
|
||||
"ZD Behind King Zora Beehive": "can_break_upper_beehive_child"
|
||||
},
|
||||
"exits": {
|
||||
"Zoras Domain": "
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
- New option `free_fly_blacklist` limits which cities can show up as a free fly location.
|
||||
- Spoiler log and hint text for maps where a species can be found now use human-friendly labels.
|
||||
- Added many item and location groups based on item type, location type, and location geography.
|
||||
- Dexsanity locations for species which evolve via item use (Fire Stone, Metal Coat, etc.) now contribute those items to
|
||||
the randomized item pool instead of Great Balls.
|
||||
- Rock smash encounters are now randomized according to your wild pokemon randomization option. These encounters are
|
||||
_not_ used for logical access (the seed will never require you to catch something through one of these encounters).
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ class PokemonRedBlueWorld(World):
|
||||
self.dexsanity_table = []
|
||||
self.trainersanity_table = []
|
||||
self.local_locs = []
|
||||
self.pc_item = None
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||
@@ -289,7 +290,9 @@ class PokemonRedBlueWorld(World):
|
||||
multiworld.random.shuffle(itempool)
|
||||
unplaced_items = []
|
||||
for i, item in enumerate(itempool):
|
||||
if item.player == loc.player and loc.can_fill(multiworld.state, item, False):
|
||||
if ((item.player == loc.player or (item.player in multiworld.groups
|
||||
and loc.player in multiworld.groups[item.player]["players"]))
|
||||
and loc.can_fill(multiworld.state, item, False)):
|
||||
if item.advancement:
|
||||
pool = progitempool
|
||||
elif item.useful:
|
||||
@@ -308,8 +311,6 @@ class PokemonRedBlueWorld(World):
|
||||
break
|
||||
else:
|
||||
unplaced_items.append(item)
|
||||
else:
|
||||
raise FillError(f"Pokemon Red and Blue local item fill failed for player {loc.player}: could not place {item.name}")
|
||||
progitempool += [item for item in unplaced_items if item.advancement]
|
||||
usefulitempool += [item for item in unplaced_items if item.useful]
|
||||
filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)]
|
||||
@@ -446,15 +447,12 @@ class PokemonRedBlueWorld(World):
|
||||
if loc.item is None:
|
||||
locs.add(loc)
|
||||
|
||||
if not self.options.key_items_only:
|
||||
loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
|
||||
if loc.item is None:
|
||||
locs.add(loc)
|
||||
|
||||
for loc in sorted(locs):
|
||||
if loc.name in self.options.priority_locations.value:
|
||||
add_item_rule(loc, lambda i: i.advancement)
|
||||
add_item_rule(loc, lambda i: i.player == self.player)
|
||||
add_item_rule(loc, lambda i: i.player == self.player
|
||||
or (i.player in self.multiworld.groups
|
||||
and self.player in self.multiworld.groups[i.player]["players"]))
|
||||
if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC":
|
||||
add_item_rule(loc, lambda i: i.name != "Oak's Parcel")
|
||||
|
||||
@@ -520,6 +518,14 @@ class PokemonRedBlueWorld(World):
|
||||
else:
|
||||
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
|
||||
|
||||
if not self.options.key_items_only:
|
||||
loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
|
||||
# Absolutely cannot have another player's item
|
||||
if loc.item is not None and loc.item.player != self.player:
|
||||
self.multiworld.itempool.append(loc.item)
|
||||
loc.item = None
|
||||
loc.place_locked_item(self.pc_item)
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, multiworld):
|
||||
# Convert all but one of each instance of a wild Pokemon to useful classification.
|
||||
|
||||
@@ -1579,6 +1579,18 @@ def create_regions(world):
|
||||
world.item_pool.append(item)
|
||||
|
||||
world.random.shuffle(world.item_pool)
|
||||
if not world.options.key_items_only:
|
||||
if "Player's House 2F - Player's PC" in world.options.exclude_locations:
|
||||
acceptable_item = lambda item: item.excludable
|
||||
elif "Player's House 2F - Player's PC" in world.options.priority_locations:
|
||||
acceptable_item = lambda item: item.advancement
|
||||
else:
|
||||
acceptable_item = lambda item: True
|
||||
for i, item in enumerate(world.item_pool):
|
||||
if acceptable_item(item):
|
||||
world.pc_item = world.item_pool.pop(i)
|
||||
break
|
||||
|
||||
advancement_items = [item.name for item in world.item_pool if item.advancement] \
|
||||
+ [item.name for item in world.multiworld.precollected_items[world.player] if
|
||||
item.advancement]
|
||||
|
||||
@@ -177,7 +177,11 @@ def randomize_rock_tunnel(data, random):
|
||||
if random.randint(0, 1):
|
||||
floor(10, 7)
|
||||
floor(11, 7)
|
||||
tall(random.randint(12, 17), 8)
|
||||
if current_map[10][13]==1:
|
||||
# (13,10) is floor
|
||||
tall(random.randint(14, 16), 8)
|
||||
else:
|
||||
tall(random.randint(12, 16), 8)
|
||||
else:
|
||||
floor(12, 5)
|
||||
floor(12, 6)
|
||||
@@ -185,8 +189,10 @@ def randomize_rock_tunnel(data, random):
|
||||
wide(17, random.randint(3, 5))
|
||||
r = random.choice([1, 3])
|
||||
floor(12, r)
|
||||
floor(12, + 1)
|
||||
|
||||
floor(12, r + 1)
|
||||
if current_map[4][12] + current_map[5][12] == 2:
|
||||
# (12,4) and (12,5) are floor
|
||||
wide(11,4)
|
||||
elif c == 2:
|
||||
r = random.randint(0, 6)
|
||||
if r == 0:
|
||||
@@ -221,6 +227,9 @@ def randomize_rock_tunnel(data, random):
|
||||
#early block
|
||||
wide(13, random.randint(2, 5))
|
||||
tall(random.randint(14, 15), 1)
|
||||
if not 1 in (current_map[1][14],current_map[2][13]):
|
||||
# wide(13,2) and tall(14,1) overlap
|
||||
single(13,2)
|
||||
elif r == 1:
|
||||
if random.randint(0, 1):
|
||||
tall(16, 5)
|
||||
@@ -243,19 +252,34 @@ def randomize_rock_tunnel(data, random):
|
||||
r = random.randint(r, 6)
|
||||
if r == 6:
|
||||
#late open
|
||||
r2 = random.randint(0, 2)
|
||||
floor(1 + (r2 * 2), 14)
|
||||
floor(2 + (r2 * 2), 14)
|
||||
if random.randint(0, 1):
|
||||
floor(1, 14)
|
||||
floor(2, 14)
|
||||
else:
|
||||
floor(3, 14)
|
||||
floor(4, 14)
|
||||
elif r == 5:
|
||||
floor(6, 12)
|
||||
floor(6, 13)
|
||||
if random.randint(0,1):
|
||||
floor(6, 12)
|
||||
floor(6, 13)
|
||||
else:
|
||||
floor(5, 14)
|
||||
floor(6, 14)
|
||||
elif r == 4:
|
||||
if random.randint(0, 1):
|
||||
floor(6, 11)
|
||||
floor(7, 11)
|
||||
else:
|
||||
floor(8, 11)
|
||||
floor(9, 11)
|
||||
if current_map[12][10]==32:
|
||||
# (10,12) is wide
|
||||
single(9, 11)
|
||||
else:
|
||||
floor(9, 11)
|
||||
if 31 in (current_map[8][6],current_map[8][7]):
|
||||
# (6,7) or (7,7) are tall
|
||||
floor(6, 10)
|
||||
wide(7, 9)
|
||||
elif r == 3:
|
||||
floor(9, 9)
|
||||
floor(9, 10)
|
||||
|
||||
@@ -150,6 +150,26 @@ sample_chao_names = [
|
||||
"Hubert",
|
||||
"Corvus",
|
||||
"Nigel",
|
||||
"Benjamin",
|
||||
"Gooey",
|
||||
"Maddy",
|
||||
"AFGNCAAP",
|
||||
"Reinhardt",
|
||||
"Claire",
|
||||
"Yoshi",
|
||||
"Peasley",
|
||||
"Faux",
|
||||
"Naija",
|
||||
"Kaiba",
|
||||
"Hat Kid",
|
||||
"TzTokJad",
|
||||
"Sora",
|
||||
"WoodMan",
|
||||
"Yachty",
|
||||
"Grieve",
|
||||
"Portia",
|
||||
"Graves",
|
||||
"Kaycee",
|
||||
]
|
||||
|
||||
totally_real_item_names = [
|
||||
@@ -240,6 +260,35 @@ totally_real_item_names = [
|
||||
"Ladder",
|
||||
|
||||
"Visible Dots",
|
||||
|
||||
"CooCoo",
|
||||
|
||||
"Blueberry",
|
||||
|
||||
"Ear of Luigi",
|
||||
|
||||
"Mega Nut",
|
||||
|
||||
"DUELIST ALLIANCE",
|
||||
"DUEL OVERLOAD",
|
||||
"POWER OF THE ELEMENTS",
|
||||
"S:P Little Knight",
|
||||
"Red-Eyes Dark Dragoon",
|
||||
|
||||
"Fire Hat",
|
||||
|
||||
"Area: Taverly",
|
||||
"Area: Meiyerditch",
|
||||
"Fire Cape",
|
||||
|
||||
"Donald Zeta Flare",
|
||||
|
||||
"Category One of a Kind",
|
||||
"Category Fuller House",
|
||||
|
||||
"Passive Camoflage",
|
||||
|
||||
"Earth Card",
|
||||
]
|
||||
|
||||
all_exits = [
|
||||
|
||||
@@ -1,6 +1,82 @@
|
||||
# Sonic Adventure 2 Battle - Changelog
|
||||
|
||||
|
||||
## v2.4 - Minigame Madness
|
||||
|
||||
### Features:
|
||||
- New Goal
|
||||
- Minigame Madness
|
||||
- Win a certain number of each type of Minigame Trap, then defeat the Finalhazard to win!
|
||||
- How many of each Minigame are required can be set by an Option
|
||||
- When the required amount of a Minigame has been received, that Minigame can be replayed in the Chao World Lobby
|
||||
- New optional Location Checks
|
||||
- Bigsanity
|
||||
- Go fishing with Big in each stage for a Location Check
|
||||
- Itemboxsanity
|
||||
- Either Extra Life Boxes or All Item Boxes
|
||||
- New Items
|
||||
- New Traps
|
||||
- Literature Trap
|
||||
- Controller Drift Trap
|
||||
- Poison Trap
|
||||
- Bee Trap
|
||||
- New Minigame Traps
|
||||
- Breakout Trap
|
||||
- Fishing Trap
|
||||
- Trivia Trap
|
||||
- Pokemon Trivia Trap
|
||||
- Pokemon Count Trap
|
||||
- Number Sequence Trap
|
||||
- Light Up Path Trap
|
||||
- Pinball Trap
|
||||
- Math Quiz Trap
|
||||
- Snake Trap
|
||||
- Input Sequence Trap
|
||||
- Trap Link
|
||||
- When you receive a trap, you send a copy of it to every other player with Trap Link enabled
|
||||
- Boss Gate Plando
|
||||
- Expert Logic Difficulty
|
||||
- Use at your own risk. This difficulty requires complete mastery of SA2.
|
||||
- Missions can now be enabled and disabled per-character, instead of just per-style
|
||||
- Minigame Difficulty can now be set to "Chaos", which selects a new difficulty randomly per-trap received
|
||||
|
||||
### Quality of Life:
|
||||
- Gate Stages and Mission Orders are now displayed in the spoiler log
|
||||
- Additional play stats are saved and displayed with the randomizer credits
|
||||
- Stage Locations progress UI now displays in multiple pages when Itemboxsanity is enabled
|
||||
- Current stage mission order and progress are now shown when paused in-level
|
||||
- Chaos Emeralds are now shown when paused in-level
|
||||
- Location Name Groups were created
|
||||
- Moved SA2B to the new Options system
|
||||
- Option Presets were created
|
||||
- Error Messages are more obvious
|
||||
|
||||
### Bug Fixes:
|
||||
- Added missing `Dry Lagoon - 12 Animals` location
|
||||
- Flying Dog boss should no longer crash when you have done at least 3 Intermediate Kart Races
|
||||
- Invincibility can no longer be received in the King Boom Boo fight, preventing a crash
|
||||
- Chaos Emeralds should no longer disproportionately end up in Cannon's Core or the final Level Gate
|
||||
- Going into submenus from the pause menu should no longer reset traps
|
||||
- `Sonic - Magic Gloves` are now plural
|
||||
- Junk items will no longer cause a crash when in a falling state
|
||||
- Chao Garden:
|
||||
- Prevent races from occasionally becoming uncompletable when using the "Prize Only" option
|
||||
- Properly allow Hero Chao to participate in Dark Races
|
||||
- Don't allow the Chao Garden to send locations when connected to an invalid server
|
||||
- Prevent the Chao Garden from resetting your life count
|
||||
- Fix Chao World Entrance Shuffle causing inaccessible Neutral Garden
|
||||
- Fix pressing the 'B' button to take you to the proper location in Chao World Entrance Shuffle
|
||||
- Prevent Chao Karate progress icon overflow
|
||||
- Prevent changing Chao Timescale while paused or while a Minigame is active
|
||||
- Logic Fixes:
|
||||
- `Mission Street - Chao Key 1` (Hard Logic) now requires no upgrades
|
||||
- `Mission Street - Chao Key 2` (Hard Logic) now requires no upgrades
|
||||
- `Crazy Gadget - Hidden 1` (Standard Logic) now requires `Sonic - Bounce Bracelet` instead of `Sonic - Light Shoes`
|
||||
- `Lost Colony - Hidden 1` (Standard Logic) now requires `Eggman - Jet Engine`
|
||||
- `Mad Space - Gold Beetle` (Standard Logic) now only requires `Rouge - Iron Boots`
|
||||
- `Cosmic Wall - Gold Beetle` (Standard and Hard Logic) now only requires `Eggman - Jet Engine`
|
||||
|
||||
|
||||
## v2.3 - The Chao Update
|
||||
|
||||
### Features:
|
||||
|
||||
+78
-34
@@ -1,18 +1,23 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
speed_characters_1 = "Sonic vs Shadow 1"
|
||||
speed_characters_2 = "Sonic vs Shadow 2"
|
||||
mech_characters_1 = "Tails vs Eggman 1"
|
||||
mech_characters_2 = "Tails vs Eggman 2"
|
||||
hunt_characters_1 = "Knuckles vs Rouge 1"
|
||||
big_foot = "F-6t BIG FOOT"
|
||||
hot_shot = "B-3x HOT SHOT"
|
||||
flying_dog = "R-1/A FLYING DOG"
|
||||
egg_golem_sonic = "Egg Golem (Sonic)"
|
||||
egg_golem_eggman = "Egg Golem (Eggman)"
|
||||
king_boom_boo = "King Boom Boo"
|
||||
from .Names import LocationName
|
||||
from .Options import GateBossPlando
|
||||
|
||||
|
||||
speed_characters_1 = "sonic vs shadow 1"
|
||||
speed_characters_2 = "sonic vs shadow 2"
|
||||
mech_characters_1 = "tails vs eggman 1"
|
||||
mech_characters_2 = "tails vs eggman 2"
|
||||
hunt_characters_1 = "knuckles vs rouge 1"
|
||||
big_foot = "big foot"
|
||||
hot_shot = "hot shot"
|
||||
flying_dog = "flying dog"
|
||||
egg_golem_sonic = "egg golem (sonic)"
|
||||
egg_golem_eggman = "egg golem (eggman)"
|
||||
king_boom_boo = "king boom boo"
|
||||
|
||||
gate_bosses_no_requirements_table = {
|
||||
speed_characters_1: 0,
|
||||
@@ -45,44 +50,83 @@ all_gate_bosses_table = {
|
||||
}
|
||||
|
||||
|
||||
boss_id_to_name = {
|
||||
0: "Sonic vs Shadow 1",
|
||||
1: "Sonic vs Shadow 2",
|
||||
2: "Tails vs Eggman 1",
|
||||
3: "Tails vs Eggman 2",
|
||||
4: "Knuckles vs Rouge 1",
|
||||
5: "F-6t BIG FOOT",
|
||||
6: "B-3x HOT SHOT",
|
||||
7: "R-1/A FLYING DOG",
|
||||
8: "Egg Golem (Sonic)",
|
||||
9: "Egg Golem (Eggman)",
|
||||
10: "King Boom Boo",
|
||||
11: "Sonic vs Shadow 1",
|
||||
12: "Sonic vs Shadow 2",
|
||||
13: "Tails vs Eggman 1",
|
||||
14: "Tails vs Eggman 2",
|
||||
15: "Knuckles vs Rouge 1",
|
||||
}
|
||||
|
||||
def get_boss_name(boss: int):
|
||||
for key, value in gate_bosses_no_requirements_table.items():
|
||||
if value == boss:
|
||||
return key
|
||||
for key, value in gate_bosses_with_requirements_table.items():
|
||||
if value == boss:
|
||||
return key
|
||||
for key, value in extra_boss_rush_bosses_table.items():
|
||||
if value == boss:
|
||||
return key
|
||||
return boss_id_to_name[boss]
|
||||
|
||||
|
||||
def boss_has_requirement(boss: int):
|
||||
return boss >= len(gate_bosses_no_requirements_table)
|
||||
|
||||
|
||||
def get_gate_bosses(multiworld: MultiWorld, world: World):
|
||||
def get_gate_bosses(world: World):
|
||||
selected_bosses: typing.List[int] = []
|
||||
boss_gates: typing.List[int] = []
|
||||
available_bosses: typing.List[str] = list(gate_bosses_no_requirements_table.keys())
|
||||
multiworld.random.shuffle(available_bosses)
|
||||
halfway = False
|
||||
world.random.shuffle(available_bosses)
|
||||
|
||||
gate_boss_plando: typing.Union[int, str] = world.options.gate_boss_plando.value
|
||||
plando_bosses = ["None", "None", "None", "None", "None"]
|
||||
if isinstance(gate_boss_plando, str):
|
||||
# boss plando
|
||||
options = gate_boss_plando.split(";")
|
||||
gate_boss_plando = GateBossPlando.options[options.pop()]
|
||||
for option in options:
|
||||
if "-" in option:
|
||||
loc, boss = option.split("-")
|
||||
boss_num = LocationName.boss_gate_names[loc]
|
||||
|
||||
if boss_num >= world.options.number_of_level_gates.value:
|
||||
# Don't reject bosses plando'd into gate bosses that won't exist
|
||||
pass
|
||||
|
||||
if boss in plando_bosses:
|
||||
# TODO: Raise error here. Duplicates not allowed
|
||||
pass
|
||||
|
||||
plando_bosses[boss_num] = boss
|
||||
|
||||
if boss in available_bosses:
|
||||
available_bosses.remove(boss)
|
||||
|
||||
for x in range(world.options.number_of_level_gates):
|
||||
if (not halfway) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
|
||||
if ("king boom boo" not in selected_bosses) and ("king boom boo" not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
|
||||
available_bosses.extend(gate_bosses_with_requirements_table)
|
||||
multiworld.random.shuffle(available_bosses)
|
||||
halfway = True
|
||||
selected_bosses.append(all_gate_bosses_table[available_bosses[0]])
|
||||
world.random.shuffle(available_bosses)
|
||||
|
||||
chosen_boss = available_bosses[0]
|
||||
if plando_bosses[x] != "None":
|
||||
available_bosses.append(plando_bosses[x])
|
||||
chosen_boss = plando_bosses[x]
|
||||
|
||||
selected_bosses.append(all_gate_bosses_table[chosen_boss])
|
||||
boss_gates.append(x + 1)
|
||||
available_bosses.remove(available_bosses[0])
|
||||
available_bosses.remove(chosen_boss)
|
||||
|
||||
bosses: typing.Dict[int, int] = dict(zip(boss_gates, selected_bosses))
|
||||
|
||||
return bosses
|
||||
|
||||
|
||||
def get_boss_rush_bosses(multiworld: MultiWorld, world: World):
|
||||
def get_boss_rush_bosses(world: World):
|
||||
|
||||
if world.options.boss_rush_shuffle == 0:
|
||||
boss_list_o = list(range(0, 16))
|
||||
@@ -92,21 +136,21 @@ def get_boss_rush_bosses(multiworld: MultiWorld, world: World):
|
||||
elif world.options.boss_rush_shuffle == 1:
|
||||
boss_list_o = list(range(0, 16))
|
||||
boss_list_s = boss_list_o.copy()
|
||||
multiworld.random.shuffle(boss_list_s)
|
||||
world.random.shuffle(boss_list_s)
|
||||
|
||||
return dict(zip(boss_list_o, boss_list_s))
|
||||
elif world.options.boss_rush_shuffle == 2:
|
||||
boss_list_o = list(range(0, 16))
|
||||
boss_list_s = [multiworld.random.choice(boss_list_o) for i in range(0, 16)]
|
||||
boss_list_s = [world.random.choice(boss_list_o) for i in range(0, 16)]
|
||||
if 10 not in boss_list_s:
|
||||
boss_list_s[multiworld.random.randint(0, 15)] = 10
|
||||
boss_list_s[world.random.randint(0, 15)] = 10
|
||||
|
||||
return dict(zip(boss_list_o, boss_list_s))
|
||||
elif world.options.boss_rush_shuffle == 3:
|
||||
boss_list_o = list(range(0, 16))
|
||||
boss_list_s = [multiworld.random.choice(boss_list_o)] * len(boss_list_o)
|
||||
boss_list_s = [world.random.choice(boss_list_o)] * len(boss_list_o)
|
||||
if 10 not in boss_list_s:
|
||||
boss_list_s[multiworld.random.randint(0, 15)] = 10
|
||||
boss_list_s[world.random.randint(0, 15)] = 10
|
||||
|
||||
return dict(zip(boss_list_o, boss_list_s))
|
||||
else:
|
||||
|
||||
+39
-18
@@ -2,7 +2,6 @@ import typing
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Names import ItemName
|
||||
from worlds.alttp import ALTTPWorld
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
@@ -14,7 +13,7 @@ class ItemData(typing.NamedTuple):
|
||||
|
||||
|
||||
class SA2BItem(Item):
|
||||
game: str = "Sonic Adventure 2: Battle"
|
||||
game: str = "Sonic Adventure 2 Battle"
|
||||
|
||||
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
|
||||
super(SA2BItem, self).__init__(name, classification, code, player)
|
||||
@@ -73,19 +72,36 @@ junk_table = {
|
||||
}
|
||||
|
||||
trap_table = {
|
||||
ItemName.omochao_trap: ItemData(0xFF0030, False, True),
|
||||
ItemName.timestop_trap: ItemData(0xFF0031, False, True),
|
||||
ItemName.confuse_trap: ItemData(0xFF0032, False, True),
|
||||
ItemName.tiny_trap: ItemData(0xFF0033, False, True),
|
||||
ItemName.gravity_trap: ItemData(0xFF0034, False, True),
|
||||
ItemName.exposition_trap: ItemData(0xFF0035, False, True),
|
||||
#ItemName.darkness_trap: ItemData(0xFF0036, False, True),
|
||||
ItemName.ice_trap: ItemData(0xFF0037, False, True),
|
||||
ItemName.slow_trap: ItemData(0xFF0038, False, True),
|
||||
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
|
||||
ItemName.reverse_trap: ItemData(0xFF003A, False, True),
|
||||
ItemName.omochao_trap: ItemData(0xFF0030, False, True),
|
||||
ItemName.timestop_trap: ItemData(0xFF0031, False, True),
|
||||
ItemName.confuse_trap: ItemData(0xFF0032, False, True),
|
||||
ItemName.tiny_trap: ItemData(0xFF0033, False, True),
|
||||
ItemName.gravity_trap: ItemData(0xFF0034, False, True),
|
||||
ItemName.exposition_trap: ItemData(0xFF0035, False, True),
|
||||
#ItemName.darkness_trap: ItemData(0xFF0036, False, True),
|
||||
ItemName.ice_trap: ItemData(0xFF0037, False, True),
|
||||
ItemName.slow_trap: ItemData(0xFF0038, False, True),
|
||||
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
|
||||
ItemName.reverse_trap: ItemData(0xFF003A, False, True),
|
||||
ItemName.literature_trap: ItemData(0xFF003B, False, True),
|
||||
ItemName.controller_drift_trap: ItemData(0xFF003C, False, True),
|
||||
ItemName.poison_trap: ItemData(0xFF003D, False, True),
|
||||
ItemName.bee_trap: ItemData(0xFF003E, False, True),
|
||||
}
|
||||
|
||||
ItemName.pong_trap: ItemData(0xFF0050, False, True),
|
||||
minigame_trap_table = {
|
||||
ItemName.pong_trap: ItemData(0xFF0050, False, True),
|
||||
ItemName.breakout_trap: ItemData(0xFF0051, False, True),
|
||||
ItemName.fishing_trap: ItemData(0xFF0052, False, True),
|
||||
ItemName.trivia_trap: ItemData(0xFF0053, False, True),
|
||||
ItemName.pokemon_trivia_trap: ItemData(0xFF0054, False, True),
|
||||
ItemName.pokemon_count_trap: ItemData(0xFF0055, False, True),
|
||||
ItemName.number_sequence_trap: ItemData(0xFF0056, False, True),
|
||||
ItemName.light_up_path_trap: ItemData(0xFF0057, False, True),
|
||||
ItemName.pinball_trap: ItemData(0xFF0058, False, True),
|
||||
ItemName.math_quiz_trap: ItemData(0xFF0059, False, True),
|
||||
ItemName.snake_trap: ItemData(0xFF005A, False, True),
|
||||
ItemName.input_sequence_trap: ItemData(0xFF005B, False, True),
|
||||
}
|
||||
|
||||
emeralds_table = {
|
||||
@@ -235,7 +251,7 @@ chaos_drives_table = {
|
||||
}
|
||||
|
||||
event_table = {
|
||||
ItemName.maria: ItemData(0xFF001D, True),
|
||||
ItemName.maria: ItemData(None, True),
|
||||
}
|
||||
|
||||
# Complete item table.
|
||||
@@ -244,6 +260,7 @@ item_table = {
|
||||
**upgrades_table,
|
||||
**junk_table,
|
||||
**trap_table,
|
||||
**minigame_trap_table,
|
||||
**emeralds_table,
|
||||
**eggs_table,
|
||||
**fruits_table,
|
||||
@@ -251,7 +268,6 @@ item_table = {
|
||||
**hats_table,
|
||||
**animals_table,
|
||||
**chaos_drives_table,
|
||||
**event_table,
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||
@@ -263,7 +279,12 @@ item_groups: typing.Dict[str, str] = {
|
||||
"Seeds": list(seeds_table.keys()),
|
||||
"Hats": list(hats_table.keys()),
|
||||
"Traps": list(trap_table.keys()),
|
||||
"Minigames": list(minigame_trap_table.keys()),
|
||||
}
|
||||
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
|
||||
try:
|
||||
from worlds.alttp import ALTTPWorld
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
+2420
-8
File diff suppressed because it is too large
Load Diff
+139
-56
@@ -119,11 +119,14 @@ mission_orders: typing.List[typing.List[int]] = [
|
||||
[4, 5, 3, 2, 1],
|
||||
]
|
||||
|
||||
### 0: Speed
|
||||
### 1: Mech
|
||||
### 2: Hunt
|
||||
### 3: Kart
|
||||
### 4: Cannon's Core
|
||||
### 0: Sonic
|
||||
### 1: Tails
|
||||
### 2: Knuckles
|
||||
### 3: Shadow
|
||||
### 4: Eggman
|
||||
### 5: Rouge
|
||||
### 6: Kart
|
||||
### 7: Cannon's Core
|
||||
level_styles: typing.List[int] = [
|
||||
0,
|
||||
2,
|
||||
@@ -133,7 +136,7 @@ level_styles: typing.List[int] = [
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
6,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
@@ -142,22 +145,22 @@ level_styles: typing.List[int] = [
|
||||
0,
|
||||
0,
|
||||
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
|
||||
4,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
5,
|
||||
4,
|
||||
4,
|
||||
5,
|
||||
3,
|
||||
6,
|
||||
3,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
|
||||
7,
|
||||
]
|
||||
|
||||
stage_name_prefixes: typing.List[str] = [
|
||||
@@ -201,21 +204,33 @@ def get_mission_count_table(multiworld: MultiWorld, world: World, player: int):
|
||||
for level in range(31):
|
||||
mission_count_table[level] = 0
|
||||
else:
|
||||
speed_active_missions = 1
|
||||
mech_active_missions = 1
|
||||
hunt_active_missions = 1
|
||||
sonic_active_missions = 1
|
||||
tails_active_missions = 1
|
||||
knuckles_active_missions = 1
|
||||
shadow_active_missions = 1
|
||||
eggman_active_missions = 1
|
||||
rouge_active_missions = 1
|
||||
kart_active_missions = 1
|
||||
cannons_core_active_missions = 1
|
||||
|
||||
for i in range(2,6):
|
||||
if getattr(world.options, "speed_mission_" + str(i), None):
|
||||
speed_active_missions += 1
|
||||
if getattr(world.options, "sonic_mission_" + str(i), None):
|
||||
sonic_active_missions += 1
|
||||
|
||||
if getattr(world.options, "mech_mission_" + str(i), None):
|
||||
mech_active_missions += 1
|
||||
if getattr(world.options, "tails_mission_" + str(i), None):
|
||||
tails_active_missions += 1
|
||||
|
||||
if getattr(world.options, "hunt_mission_" + str(i), None):
|
||||
hunt_active_missions += 1
|
||||
if getattr(world.options, "knuckles_mission_" + str(i), None):
|
||||
knuckles_active_missions += 1
|
||||
|
||||
if getattr(world.options, "shadow_mission_" + str(i), None):
|
||||
shadow_active_missions += 1
|
||||
|
||||
if getattr(world.options, "eggman_mission_" + str(i), None):
|
||||
eggman_active_missions += 1
|
||||
|
||||
if getattr(world.options, "rouge_mission_" + str(i), None):
|
||||
rouge_active_missions += 1
|
||||
|
||||
if getattr(world.options, "kart_mission_" + str(i), None):
|
||||
kart_active_missions += 1
|
||||
@@ -223,16 +238,22 @@ def get_mission_count_table(multiworld: MultiWorld, world: World, player: int):
|
||||
if getattr(world.options, "cannons_core_mission_" + str(i), None):
|
||||
cannons_core_active_missions += 1
|
||||
|
||||
speed_active_missions = min(speed_active_missions, world.options.speed_mission_count.value)
|
||||
mech_active_missions = min(mech_active_missions, world.options.mech_mission_count.value)
|
||||
hunt_active_missions = min(hunt_active_missions, world.options.hunt_mission_count.value)
|
||||
sonic_active_missions = min(sonic_active_missions, world.options.sonic_mission_count.value)
|
||||
tails_active_missions = min(tails_active_missions, world.options.tails_mission_count.value)
|
||||
knuckles_active_missions = min(knuckles_active_missions, world.options.knuckles_mission_count.value)
|
||||
shadow_active_missions = min(shadow_active_missions, world.options.sonic_mission_count.value)
|
||||
eggman_active_missions = min(eggman_active_missions, world.options.eggman_mission_count.value)
|
||||
rouge_active_missions = min(rouge_active_missions, world.options.rouge_mission_count.value)
|
||||
kart_active_missions = min(kart_active_missions, world.options.kart_mission_count.value)
|
||||
cannons_core_active_missions = min(cannons_core_active_missions, world.options.cannons_core_mission_count.value)
|
||||
|
||||
active_missions: typing.List[typing.List[int]] = [
|
||||
speed_active_missions,
|
||||
mech_active_missions,
|
||||
hunt_active_missions,
|
||||
sonic_active_missions,
|
||||
tails_active_missions,
|
||||
knuckles_active_missions,
|
||||
shadow_active_missions,
|
||||
eggman_active_missions,
|
||||
rouge_active_missions,
|
||||
kart_active_missions,
|
||||
cannons_core_active_missions
|
||||
]
|
||||
@@ -252,22 +273,34 @@ def get_mission_table(multiworld: MultiWorld, world: World, player: int):
|
||||
for level in range(31):
|
||||
mission_table[level] = 0
|
||||
else:
|
||||
speed_active_missions: typing.List[int] = [1]
|
||||
mech_active_missions: typing.List[int] = [1]
|
||||
hunt_active_missions: typing.List[int] = [1]
|
||||
sonic_active_missions: typing.List[int] = [1]
|
||||
tails_active_missions: typing.List[int] = [1]
|
||||
knuckles_active_missions: typing.List[int] = [1]
|
||||
shadow_active_missions: typing.List[int] = [1]
|
||||
eggman_active_missions: typing.List[int] = [1]
|
||||
rouge_active_missions: typing.List[int] = [1]
|
||||
kart_active_missions: typing.List[int] = [1]
|
||||
cannons_core_active_missions: typing.List[int] = [1]
|
||||
|
||||
# Add included missions
|
||||
for i in range(2,6):
|
||||
if getattr(world.options, "speed_mission_" + str(i), None):
|
||||
speed_active_missions.append(i)
|
||||
if getattr(world.options, "sonic_mission_" + str(i), None):
|
||||
sonic_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "mech_mission_" + str(i), None):
|
||||
mech_active_missions.append(i)
|
||||
if getattr(world.options, "tails_mission_" + str(i), None):
|
||||
tails_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "hunt_mission_" + str(i), None):
|
||||
hunt_active_missions.append(i)
|
||||
if getattr(world.options, "knuckles_mission_" + str(i), None):
|
||||
knuckles_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "shadow_mission_" + str(i), None):
|
||||
shadow_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "eggman_mission_" + str(i), None):
|
||||
eggman_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "rouge_mission_" + str(i), None):
|
||||
rouge_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "kart_mission_" + str(i), None):
|
||||
kart_active_missions.append(i)
|
||||
@@ -276,9 +309,12 @@ def get_mission_table(multiworld: MultiWorld, world: World, player: int):
|
||||
cannons_core_active_missions.append(i)
|
||||
|
||||
active_missions: typing.List[typing.List[int]] = [
|
||||
speed_active_missions,
|
||||
mech_active_missions,
|
||||
hunt_active_missions,
|
||||
sonic_active_missions,
|
||||
tails_active_missions,
|
||||
knuckles_active_missions,
|
||||
shadow_active_missions,
|
||||
eggman_active_missions,
|
||||
rouge_active_missions,
|
||||
kart_active_missions,
|
||||
cannons_core_active_missions
|
||||
]
|
||||
@@ -328,13 +364,60 @@ def get_mission_table(multiworld: MultiWorld, world: World, player: int):
|
||||
|
||||
|
||||
def get_first_and_last_cannons_core_missions(mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
|
||||
mission_count = mission_count_map[30]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||
stage_prefix: str = stage_name_prefixes[30]
|
||||
mission_count = mission_count_map[30]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||
stage_prefix: str = stage_name_prefixes[30]
|
||||
|
||||
first_mission_number = mission_order[0]
|
||||
last_mission_number = mission_order[mission_count - 1]
|
||||
first_location_name: str = stage_prefix + str(first_mission_number)
|
||||
last_location_name: str = stage_prefix + str(last_mission_number)
|
||||
first_mission_number = mission_order[0]
|
||||
last_mission_number = mission_order[mission_count - 1]
|
||||
first_location_name: str = stage_prefix + str(first_mission_number)
|
||||
last_location_name: str = stage_prefix + str(last_mission_number)
|
||||
|
||||
return first_location_name, last_location_name
|
||||
return first_location_name, last_location_name
|
||||
|
||||
|
||||
def print_mission_orders_to_spoiler(mission_map: typing.Dict[int, int],
|
||||
mission_count_map: typing.Dict[int, int],
|
||||
shuffled_region_list: typing.Dict[int, int],
|
||||
levels_per_gate: typing.Dict[int, int],
|
||||
player_name: str,
|
||||
spoiler_handle: typing.TextIO):
|
||||
spoiler_handle.write("\n")
|
||||
header_text = "SA2 Mission Orders for {}:\n"
|
||||
header_text = header_text.format(player_name)
|
||||
spoiler_handle.write(header_text)
|
||||
|
||||
level_index = 0
|
||||
for gate_idx in range(len(levels_per_gate)):
|
||||
gate_len = levels_per_gate[gate_idx]
|
||||
gate_levels = shuffled_region_list[int(level_index):int(level_index+gate_len)]
|
||||
gate_levels.sort()
|
||||
|
||||
gate_text = "Gate {}:\n"
|
||||
gate_text = gate_text.format(gate_idx)
|
||||
spoiler_handle.write(gate_text)
|
||||
|
||||
for i in range(len(gate_levels)):
|
||||
stage = gate_levels[i]
|
||||
mission_count = mission_count_map[stage]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[stage]]
|
||||
stage_prefix: str = stage_name_prefixes[stage]
|
||||
|
||||
for mission in range(mission_count):
|
||||
stage_prefix += str(mission_order[mission]) + " "
|
||||
|
||||
spoiler_handle.write(stage_prefix)
|
||||
spoiler_handle.write("\n")
|
||||
|
||||
level_index += gate_len
|
||||
spoiler_handle.write("\n")
|
||||
|
||||
mission_count = mission_count_map[30]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||
stage_prefix: str = stage_name_prefixes[30]
|
||||
|
||||
for mission in range(mission_count):
|
||||
stage_prefix += str(mission_order[mission]) + " "
|
||||
|
||||
spoiler_handle.write(stage_prefix)
|
||||
spoiler_handle.write("\n\n")
|
||||
|
||||
@@ -5,7 +5,7 @@ emblem = "Emblem"
|
||||
market_token = "Chao Coin"
|
||||
|
||||
# Upgrade Definitions
|
||||
sonic_gloves = "Sonic - Magic Glove"
|
||||
sonic_gloves = "Sonic - Magic Gloves"
|
||||
sonic_light_shoes = "Sonic - Light Shoes"
|
||||
sonic_ancient_light = "Sonic - Ancient Light"
|
||||
sonic_bounce_bracelet = "Sonic - Bounce Bracelet"
|
||||
@@ -51,19 +51,34 @@ invincibility = "Invincibility"
|
||||
|
||||
|
||||
# Traps
|
||||
omochao_trap = "OmoTrap"
|
||||
timestop_trap = "Chaos Control Trap"
|
||||
confuse_trap = "Confusion Trap"
|
||||
tiny_trap = "Tiny Trap"
|
||||
gravity_trap = "Gravity Trap"
|
||||
exposition_trap = "Exposition Trap"
|
||||
darkness_trap = "Darkness Trap"
|
||||
ice_trap = "Ice Trap"
|
||||
slow_trap = "Slow Trap"
|
||||
cutscene_trap = "Cutscene Trap"
|
||||
reverse_trap = "Reverse Trap"
|
||||
omochao_trap = "OmoTrap"
|
||||
timestop_trap = "Chaos Control Trap"
|
||||
confuse_trap = "Confusion Trap"
|
||||
tiny_trap = "Tiny Trap"
|
||||
gravity_trap = "Gravity Trap"
|
||||
exposition_trap = "Exposition Trap"
|
||||
darkness_trap = "Darkness Trap"
|
||||
ice_trap = "Ice Trap"
|
||||
slow_trap = "Slow Trap"
|
||||
cutscene_trap = "Cutscene Trap"
|
||||
reverse_trap = "Reverse Trap"
|
||||
literature_trap = "Literature Trap"
|
||||
controller_drift_trap = "Controller Drift Trap"
|
||||
poison_trap = "Poison Trap"
|
||||
bee_trap = "Bee Trap"
|
||||
|
||||
pong_trap = "Pong Trap"
|
||||
pong_trap = "Pong Trap"
|
||||
breakout_trap = "Breakout Trap"
|
||||
fishing_trap = "Fishing Trap"
|
||||
trivia_trap = "Trivia Trap"
|
||||
pokemon_trivia_trap = "Pokemon Trivia Trap"
|
||||
pokemon_count_trap = "Pokemon Count Trap"
|
||||
number_sequence_trap = "Number Sequence Trap"
|
||||
light_up_path_trap = "Light Up Path Trap"
|
||||
pinball_trap = "Pinball Trap"
|
||||
math_quiz_trap = "Math Quiz Trap"
|
||||
snake_trap = "Snake Trap"
|
||||
input_sequence_trap = "Input Sequence Trap"
|
||||
|
||||
|
||||
# Chaos Emeralds
|
||||
|
||||
+1448
-892
File diff suppressed because it is too large
Load Diff
+493
-86
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
||||
from Options import Choice, Range, Option, OptionGroup, Toggle, DeathLink, DefaultOnToggle, PerGameCommonOptions, PlandoBosses
|
||||
|
||||
from .Names import LocationName
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
@@ -22,6 +24,8 @@ class Goal(Choice):
|
||||
Boss Rush Chaos Emerald Hunt: Find the Seven Chaos Emeralds, then beat all of the bosses in the Boss Rush, ending with Finalhazard
|
||||
|
||||
Chaos Chao: Raise a Chaos Chao to win
|
||||
|
||||
Minigame Madness: Win a certain amount of each Minigame Trap, then defeat Finalhazard
|
||||
"""
|
||||
display_name = "Goal"
|
||||
option_biolizard = 0
|
||||
@@ -32,6 +36,7 @@ class Goal(Choice):
|
||||
option_cannons_core_boss_rush = 5
|
||||
option_boss_rush_chaos_emerald_hunt = 6
|
||||
option_chaos_chao = 7
|
||||
option_minigame_madness = 8
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -71,6 +76,66 @@ class BossRushShuffle(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class GateBossPlando(PlandoBosses):
|
||||
"""
|
||||
Possible Locations:
|
||||
"Gate 1 Boss"
|
||||
"Gate 2 Boss"
|
||||
"Gate 3 Boss"
|
||||
"Gate 4 Boss"
|
||||
"Gate 5 Boss"
|
||||
|
||||
Possible Bosses:
|
||||
"Sonic vs Shadow 1"
|
||||
"Sonic vs Shadow 2"
|
||||
"Tails vs Eggman 1"
|
||||
"Tails vs Eggman 2"
|
||||
"Knuckles vs Rouge 1"
|
||||
"BIG FOOT"
|
||||
"HOT SHOT"
|
||||
"FLYING DOG"
|
||||
"Egg Golem (Sonic)"
|
||||
"Egg Golem (Eggman)"
|
||||
"King Boom Boo"
|
||||
"""
|
||||
bosses = frozenset(LocationName.boss_names.keys())
|
||||
|
||||
locations = frozenset(LocationName.boss_gate_names.keys())
|
||||
|
||||
duplicate_bosses = False
|
||||
|
||||
@classmethod
|
||||
def can_place_boss(cls, boss: str, location: str) -> bool:
|
||||
return True
|
||||
|
||||
display_name = "Boss Shuffle"
|
||||
option_plando = 0
|
||||
|
||||
|
||||
class MinigameMadnessRequirement(Range):
|
||||
"""
|
||||
Determines how many of each Minigame Trap must be won (for Minigame Madness goal)
|
||||
|
||||
Receiving this many of a Minigame Trap will allow you to replay that minigame at-will in the Chao World lobby
|
||||
"""
|
||||
display_name = "Minigame Madness Trap Requirement"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 3
|
||||
|
||||
|
||||
class MinigameMadnessMinimum(Range):
|
||||
"""
|
||||
Determines the minimum number of each Minigame Trap that are created (for Minigame Madness goal)
|
||||
|
||||
At least this many of each trap will be created as "Progression Traps", regardless of other trap option selections
|
||||
"""
|
||||
display_name = "Minigame Madness Trap Minimum"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 5
|
||||
|
||||
|
||||
class BaseTrapWeight(Choice):
|
||||
"""
|
||||
Base Class for Trap Weights
|
||||
@@ -159,6 +224,34 @@ class ReverseTrapWeight(BaseTrapWeight):
|
||||
display_name = "Reverse Trap Weight"
|
||||
|
||||
|
||||
class LiteratureTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to read
|
||||
"""
|
||||
display_name = "Literature Trap Weight"
|
||||
|
||||
|
||||
class ControllerDriftTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which causes your control sticks to drift
|
||||
"""
|
||||
display_name = "Controller Drift Trap Weight"
|
||||
|
||||
|
||||
class PoisonTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which causes you to lose rings over time
|
||||
"""
|
||||
display_name = "Poison Trap Weight"
|
||||
|
||||
|
||||
class BeeTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which spawns a swarm of bees
|
||||
"""
|
||||
display_name = "Bee Trap Weight"
|
||||
|
||||
|
||||
class PongTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pong minigame
|
||||
@@ -166,14 +259,106 @@ class PongTrapWeight(BaseTrapWeight):
|
||||
display_name = "Pong Trap Weight"
|
||||
|
||||
|
||||
class BreakoutTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Breakout minigame
|
||||
"""
|
||||
display_name = "Breakout Trap Weight"
|
||||
|
||||
|
||||
class FishingTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Fishing minigame
|
||||
"""
|
||||
display_name = "Fishing Trap Weight"
|
||||
|
||||
|
||||
class TriviaTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Trivia minigame
|
||||
"""
|
||||
display_name = "Trivia Trap Weight"
|
||||
|
||||
|
||||
class PokemonTriviaTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pokemon Trivia minigame
|
||||
"""
|
||||
display_name = "Pokemon Trivia Trap Weight"
|
||||
|
||||
|
||||
class PokemonCountTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pokemon Count minigame
|
||||
"""
|
||||
display_name = "Pokemon Count Trap Weight"
|
||||
|
||||
|
||||
class NumberSequenceTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Number Sequence minigame
|
||||
"""
|
||||
display_name = "Number Sequence Trap Weight"
|
||||
|
||||
|
||||
class LightUpPathTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Light Up Path minigame
|
||||
"""
|
||||
display_name = "Light Up Path Trap Weight"
|
||||
|
||||
|
||||
class PinballTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pinball minigame
|
||||
"""
|
||||
display_name = "Pinball Trap Weight"
|
||||
|
||||
|
||||
class MathQuizTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to solve a math problem
|
||||
"""
|
||||
display_name = "Math Quiz Trap Weight"
|
||||
|
||||
|
||||
class SnakeTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Snake minigame
|
||||
"""
|
||||
display_name = "Snake Trap Weight"
|
||||
|
||||
|
||||
class InputSequenceTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to press a sequence of inputs
|
||||
"""
|
||||
display_name = "Input Sequence Trap Weight"
|
||||
|
||||
|
||||
class MinigameTrapDifficulty(Choice):
|
||||
"""
|
||||
How difficult any Minigame-style traps are
|
||||
Chaos causes the difficulty to be random per-minigame
|
||||
"""
|
||||
display_name = "Minigame Trap Difficulty"
|
||||
option_easy = 0
|
||||
option_medium = 1
|
||||
option_hard = 2
|
||||
option_chaos = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class BigFishingDifficulty(Choice):
|
||||
"""
|
||||
How difficult Big's Fishing Minigames are
|
||||
Chaos causes the difficulty to be random per-minigame
|
||||
"""
|
||||
display_name = "Big Fishing Difficulty"
|
||||
option_easy = 0
|
||||
option_medium = 1
|
||||
option_hard = 2
|
||||
option_chaos = 3
|
||||
default = 1
|
||||
|
||||
|
||||
@@ -197,7 +382,7 @@ class TrapFillPercentage(Range):
|
||||
default = 0
|
||||
|
||||
|
||||
class Keysanity(Toggle):
|
||||
class Keysanity(DefaultOnToggle):
|
||||
"""
|
||||
Determines whether picking up Chao Keys grants checks
|
||||
(86 Locations)
|
||||
@@ -225,7 +410,7 @@ class Whistlesanity(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class Beetlesanity(Toggle):
|
||||
class Beetlesanity(DefaultOnToggle):
|
||||
"""
|
||||
Determines whether destroying Gold Beetles grants checks
|
||||
(27 Locations)
|
||||
@@ -244,13 +429,35 @@ class Omosanity(Toggle):
|
||||
class Animalsanity(Toggle):
|
||||
"""
|
||||
Determines whether unique counts of animals grant checks.
|
||||
(421 Locations)
|
||||
(422 Locations)
|
||||
|
||||
ALL animals must be collected in a single run of a mission to get all checks.
|
||||
"""
|
||||
display_name = "Animalsanity"
|
||||
|
||||
|
||||
class ItemBoxsanity(Choice):
|
||||
"""
|
||||
Determines whether collecting Item Boxes grants checks
|
||||
None: No Item Boxes grant checks
|
||||
Extra Lives: Extra Life Boxes grant checks (94 Locations)
|
||||
All: All Item Boxes grant checks (502 Locations Total)
|
||||
"""
|
||||
display_name = "Itemboxsanity"
|
||||
option_none = 0
|
||||
option_extra_lives = 1
|
||||
option_all = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class Bigsanity(Toggle):
|
||||
"""
|
||||
Determines whether helping Big fish grants checks.
|
||||
(32 Locations)
|
||||
"""
|
||||
display_name = "Bigsanity"
|
||||
|
||||
|
||||
class KartRaceChecks(Choice):
|
||||
"""
|
||||
Determines whether Kart Race Mode grants checks
|
||||
@@ -313,7 +520,7 @@ class LevelGateCosts(Choice):
|
||||
option_low = 0
|
||||
option_medium = 1
|
||||
option_high = 2
|
||||
default = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class MaximumEmblemCap(Range):
|
||||
@@ -523,109 +730,214 @@ class BaseMissionCount(Range):
|
||||
default = 2
|
||||
|
||||
|
||||
class SpeedMissionCount(BaseMissionCount):
|
||||
class SonicMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Sonic and Shadow stages
|
||||
The number of active missions to include for Sonic stages
|
||||
"""
|
||||
display_name = "Speed Mission Count"
|
||||
display_name = "Sonic Mission Count"
|
||||
|
||||
|
||||
class SpeedMission2(DefaultOnToggle):
|
||||
class SonicMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow 100 rings missions should be included
|
||||
Determines if the Sonic 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 2"
|
||||
display_name = "Sonic Mission 2"
|
||||
|
||||
|
||||
class SpeedMission3(DefaultOnToggle):
|
||||
class SonicMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow lost chao missions should be included
|
||||
Determines if the Sonic lost chao missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 3"
|
||||
display_name = "Sonic Mission 3"
|
||||
|
||||
|
||||
class SpeedMission4(DefaultOnToggle):
|
||||
class SonicMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow time trial missions should be included
|
||||
Determines if the Sonic time trial missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 4"
|
||||
display_name = "Sonic Mission 4"
|
||||
|
||||
|
||||
class SpeedMission5(DefaultOnToggle):
|
||||
class SonicMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow hard missions should be included
|
||||
Determines if the Sonic hard missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 5"
|
||||
display_name = "Sonic Mission 5"
|
||||
|
||||
|
||||
class MechMissionCount(BaseMissionCount):
|
||||
class ShadowMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Tails and Eggman stages
|
||||
The number of active missions to include for Shadow stages
|
||||
"""
|
||||
display_name = "Mech Mission Count"
|
||||
display_name = "Shadow Mission Count"
|
||||
|
||||
|
||||
class MechMission2(DefaultOnToggle):
|
||||
class ShadowMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman 100 rings missions should be included
|
||||
Determines if the Shadow 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 2"
|
||||
display_name = "Shadow Mission 2"
|
||||
|
||||
|
||||
class MechMission3(DefaultOnToggle):
|
||||
class ShadowMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman lost chao missions should be included
|
||||
Determines if the Shadow lost chao missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 3"
|
||||
display_name = "Shadow Mission 3"
|
||||
|
||||
|
||||
class MechMission4(DefaultOnToggle):
|
||||
class ShadowMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman time trial missions should be included
|
||||
Determines if the Shadow time trial missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 4"
|
||||
display_name = "Shadow Mission 4"
|
||||
|
||||
|
||||
class MechMission5(DefaultOnToggle):
|
||||
class ShadowMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman hard missions should be included
|
||||
Determines if the Shadow hard missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 5"
|
||||
display_name = "Shadow Mission 5"
|
||||
|
||||
|
||||
class HuntMissionCount(BaseMissionCount):
|
||||
class TailsMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Knuckles and Rouge stages
|
||||
The number of active missions to include for Tails stages
|
||||
"""
|
||||
display_name = "Hunt Mission Count"
|
||||
display_name = "Tails Mission Count"
|
||||
|
||||
|
||||
class HuntMission2(DefaultOnToggle):
|
||||
class TailsMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge 100 rings missions should be included
|
||||
Determines if the Tails 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 2"
|
||||
display_name = "Tails Mission 2"
|
||||
|
||||
|
||||
class HuntMission3(DefaultOnToggle):
|
||||
class TailsMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge lost chao missions should be included
|
||||
Determines if the Tails lost chao missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 3"
|
||||
display_name = "Tails Mission 3"
|
||||
|
||||
|
||||
class HuntMission4(DefaultOnToggle):
|
||||
class TailsMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge time trial missions should be included
|
||||
Determines if the Tails time trial missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 4"
|
||||
display_name = "Tails Mission 4"
|
||||
|
||||
|
||||
class HuntMission5(DefaultOnToggle):
|
||||
class TailsMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge hard missions should be included
|
||||
Determines if the Tails hard missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 5"
|
||||
display_name = "Tails Mission 5"
|
||||
|
||||
|
||||
class EggmanMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Eggman stages
|
||||
"""
|
||||
display_name = "Eggman Mission Count"
|
||||
|
||||
|
||||
class EggmanMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 2"
|
||||
|
||||
|
||||
class EggmanMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman lost chao missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 3"
|
||||
|
||||
|
||||
class EggmanMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman time trial missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 4"
|
||||
|
||||
|
||||
class EggmanMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman hard missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 5"
|
||||
|
||||
|
||||
class KnucklesMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Knuckles stages
|
||||
"""
|
||||
display_name = "Knuckles Mission Count"
|
||||
|
||||
|
||||
class KnucklesMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 2"
|
||||
|
||||
|
||||
class KnucklesMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles lost chao missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 3"
|
||||
|
||||
|
||||
class KnucklesMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles time trial missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 4"
|
||||
|
||||
|
||||
class KnucklesMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles hard missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 5"
|
||||
|
||||
|
||||
class RougeMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Rouge stages
|
||||
"""
|
||||
display_name = "Rouge Mission Count"
|
||||
|
||||
|
||||
class RougeMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 2"
|
||||
|
||||
|
||||
class RougeMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge lost chao missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 3"
|
||||
|
||||
|
||||
class RougeMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge time trial missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 4"
|
||||
|
||||
|
||||
class RougeMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge hard missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 5"
|
||||
|
||||
|
||||
class KartMissionCount(BaseMissionCount):
|
||||
@@ -706,7 +1018,7 @@ class RingLoss(Choice):
|
||||
|
||||
Modern: You lose 20 rings when hit
|
||||
|
||||
OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!)
|
||||
OHKO: You die immediately when hit (NOTE: Some Hard or Expert Logic tricks may require damage boosts!)
|
||||
"""
|
||||
display_name = "Ring Loss"
|
||||
option_classic = 0
|
||||
@@ -729,6 +1041,16 @@ class RingLink(Toggle):
|
||||
display_name = "Ring Link"
|
||||
|
||||
|
||||
class TrapLink(Toggle):
|
||||
"""
|
||||
Whether your received traps are linked to other players
|
||||
|
||||
You will also receive any linked traps from other players with Trap Link enabled,
|
||||
if you have a weight above "none" set for that trap
|
||||
"""
|
||||
display_name = "Trap Link"
|
||||
|
||||
|
||||
class SADXMusic(Choice):
|
||||
"""
|
||||
Whether the randomizer will include Sonic Adventure DX Music in the music pool
|
||||
@@ -823,11 +1145,14 @@ class LogicDifficulty(Choice):
|
||||
|
||||
Standard: The logic assumes the "intended" usage of Upgrades to progress through levels
|
||||
|
||||
Hard: Some simple skips or sequence breaks may be required
|
||||
Hard: Some simple skips or sequence breaks may be required, but no out-of-bounds
|
||||
|
||||
Expert: If it is humanly possible, it may be required
|
||||
"""
|
||||
display_name = "Logic Difficulty"
|
||||
option_standard = 0
|
||||
option_hard = 1
|
||||
option_expert = 2
|
||||
default = 0
|
||||
|
||||
|
||||
@@ -835,6 +1160,8 @@ sa2b_option_groups = [
|
||||
OptionGroup("General Options", [
|
||||
Goal,
|
||||
BossRushShuffle,
|
||||
MinigameMadnessRequirement,
|
||||
MinigameMadnessMinimum,
|
||||
LogicDifficulty,
|
||||
RequiredRank,
|
||||
MaximumEmblemCap,
|
||||
@@ -854,6 +1181,8 @@ sa2b_option_groups = [
|
||||
Beetlesanity,
|
||||
Omosanity,
|
||||
Animalsanity,
|
||||
ItemBoxsanity,
|
||||
Bigsanity,
|
||||
KartRaceChecks,
|
||||
]),
|
||||
OptionGroup("Chao", [
|
||||
@@ -885,29 +1214,68 @@ sa2b_option_groups = [
|
||||
SlowTrapWeight,
|
||||
CutsceneTrapWeight,
|
||||
ReverseTrapWeight,
|
||||
LiteratureTrapWeight,
|
||||
ControllerDriftTrapWeight,
|
||||
PoisonTrapWeight,
|
||||
BeeTrapWeight,
|
||||
]),
|
||||
OptionGroup("Minigames", [
|
||||
PongTrapWeight,
|
||||
BreakoutTrapWeight,
|
||||
FishingTrapWeight,
|
||||
TriviaTrapWeight,
|
||||
PokemonTriviaTrapWeight,
|
||||
PokemonCountTrapWeight,
|
||||
NumberSequenceTrapWeight,
|
||||
LightUpPathTrapWeight,
|
||||
PinballTrapWeight,
|
||||
MathQuizTrapWeight,
|
||||
SnakeTrapWeight,
|
||||
InputSequenceTrapWeight,
|
||||
MinigameTrapDifficulty,
|
||||
BigFishingDifficulty,
|
||||
]),
|
||||
OptionGroup("Speed Missions", [
|
||||
SpeedMissionCount,
|
||||
SpeedMission2,
|
||||
SpeedMission3,
|
||||
SpeedMission4,
|
||||
SpeedMission5,
|
||||
OptionGroup("Sonic Missions", [
|
||||
SonicMissionCount,
|
||||
SonicMission2,
|
||||
SonicMission3,
|
||||
SonicMission4,
|
||||
SonicMission5,
|
||||
]),
|
||||
OptionGroup("Mech Missions", [
|
||||
MechMissionCount,
|
||||
MechMission2,
|
||||
MechMission3,
|
||||
MechMission4,
|
||||
MechMission5,
|
||||
OptionGroup("Shadow Missions", [
|
||||
ShadowMissionCount,
|
||||
ShadowMission2,
|
||||
ShadowMission3,
|
||||
ShadowMission4,
|
||||
ShadowMission5,
|
||||
]),
|
||||
OptionGroup("Hunt Missions", [
|
||||
HuntMissionCount,
|
||||
HuntMission2,
|
||||
HuntMission3,
|
||||
HuntMission4,
|
||||
HuntMission5,
|
||||
OptionGroup("Tails Missions", [
|
||||
TailsMissionCount,
|
||||
TailsMission2,
|
||||
TailsMission3,
|
||||
TailsMission4,
|
||||
TailsMission5,
|
||||
]),
|
||||
OptionGroup("Eggman Missions", [
|
||||
EggmanMissionCount,
|
||||
EggmanMission2,
|
||||
EggmanMission3,
|
||||
EggmanMission4,
|
||||
EggmanMission5,
|
||||
]),
|
||||
OptionGroup("Knuckles Missions", [
|
||||
KnucklesMissionCount,
|
||||
KnucklesMission2,
|
||||
KnucklesMission3,
|
||||
KnucklesMission4,
|
||||
KnucklesMission5,
|
||||
]),
|
||||
OptionGroup("Rouge Missions", [
|
||||
RougeMissionCount,
|
||||
RougeMission2,
|
||||
RougeMission3,
|
||||
RougeMission4,
|
||||
RougeMission5,
|
||||
]),
|
||||
OptionGroup("Kart Missions", [
|
||||
KartMissionCount,
|
||||
@@ -931,11 +1299,13 @@ sa2b_option_groups = [
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SA2BOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
boss_rush_shuffle: BossRushShuffle
|
||||
minigame_madness_requirement: MinigameMadnessRequirement
|
||||
minigame_madness_minimum: MinigameMadnessMinimum
|
||||
gate_boss_plando: GateBossPlando
|
||||
logic_difficulty: LogicDifficulty
|
||||
required_rank: RequiredRank
|
||||
max_emblem_cap: MaximumEmblemCap
|
||||
@@ -953,6 +1323,8 @@ class SA2BOptions(PerGameCommonOptions):
|
||||
beetlesanity: Beetlesanity
|
||||
omosanity: Omosanity
|
||||
animalsanity: Animalsanity
|
||||
itemboxsanity: ItemBoxsanity
|
||||
bigsanity: Bigsanity
|
||||
kart_race_checks: KartRaceChecks
|
||||
|
||||
black_market_slots: BlackMarketSlots
|
||||
@@ -983,31 +1355,65 @@ class SA2BOptions(PerGameCommonOptions):
|
||||
slow_trap_weight: SlowTrapWeight
|
||||
cutscene_trap_weight: CutsceneTrapWeight
|
||||
reverse_trap_weight: ReverseTrapWeight
|
||||
literature_trap_weight: LiteratureTrapWeight
|
||||
controller_drift_trap_weight: ControllerDriftTrapWeight
|
||||
poison_trap_weight: PoisonTrapWeight
|
||||
bee_trap_weight: BeeTrapWeight
|
||||
pong_trap_weight: PongTrapWeight
|
||||
breakout_trap_weight: BreakoutTrapWeight
|
||||
fishing_trap_weight: FishingTrapWeight
|
||||
trivia_trap_weight: TriviaTrapWeight
|
||||
pokemon_trivia_trap_weight: PokemonTriviaTrapWeight
|
||||
pokemon_count_trap_weight: PokemonCountTrapWeight
|
||||
number_sequence_trap_weight: NumberSequenceTrapWeight
|
||||
light_up_path_trap_weight: LightUpPathTrapWeight
|
||||
pinball_trap_weight: PinballTrapWeight
|
||||
math_quiz_trap_weight: MathQuizTrapWeight
|
||||
snake_trap_weight: SnakeTrapWeight
|
||||
input_sequence_trap_weight: InputSequenceTrapWeight
|
||||
minigame_trap_difficulty: MinigameTrapDifficulty
|
||||
big_fishing_difficulty: BigFishingDifficulty
|
||||
|
||||
sadx_music: SADXMusic
|
||||
music_shuffle: MusicShuffle
|
||||
voice_shuffle: VoiceShuffle
|
||||
narrator: Narrator
|
||||
|
||||
speed_mission_count: SpeedMissionCount
|
||||
speed_mission_2: SpeedMission2
|
||||
speed_mission_3: SpeedMission3
|
||||
speed_mission_4: SpeedMission4
|
||||
speed_mission_5: SpeedMission5
|
||||
sonic_mission_count: SonicMissionCount
|
||||
sonic_mission_2: SonicMission2
|
||||
sonic_mission_3: SonicMission3
|
||||
sonic_mission_4: SonicMission4
|
||||
sonic_mission_5: SonicMission5
|
||||
|
||||
mech_mission_count: MechMissionCount
|
||||
mech_mission_2: MechMission2
|
||||
mech_mission_3: MechMission3
|
||||
mech_mission_4: MechMission4
|
||||
mech_mission_5: MechMission5
|
||||
shadow_mission_count: ShadowMissionCount
|
||||
shadow_mission_2: ShadowMission2
|
||||
shadow_mission_3: ShadowMission3
|
||||
shadow_mission_4: ShadowMission4
|
||||
shadow_mission_5: ShadowMission5
|
||||
|
||||
hunt_mission_count: HuntMissionCount
|
||||
hunt_mission_2: HuntMission2
|
||||
hunt_mission_3: HuntMission3
|
||||
hunt_mission_4: HuntMission4
|
||||
hunt_mission_5: HuntMission5
|
||||
tails_mission_count: TailsMissionCount
|
||||
tails_mission_2: TailsMission2
|
||||
tails_mission_3: TailsMission3
|
||||
tails_mission_4: TailsMission4
|
||||
tails_mission_5: TailsMission5
|
||||
|
||||
eggman_mission_count: EggmanMissionCount
|
||||
eggman_mission_2: EggmanMission2
|
||||
eggman_mission_3: EggmanMission3
|
||||
eggman_mission_4: EggmanMission4
|
||||
eggman_mission_5: EggmanMission5
|
||||
|
||||
knuckles_mission_count: KnucklesMissionCount
|
||||
knuckles_mission_2: KnucklesMission2
|
||||
knuckles_mission_3: KnucklesMission3
|
||||
knuckles_mission_4: KnucklesMission4
|
||||
knuckles_mission_5: KnucklesMission5
|
||||
|
||||
rouge_mission_count: RougeMissionCount
|
||||
rouge_mission_2: RougeMission2
|
||||
rouge_mission_3: RougeMission3
|
||||
rouge_mission_4: RougeMission4
|
||||
rouge_mission_5: RougeMission5
|
||||
|
||||
kart_mission_count: KartMissionCount
|
||||
kart_mission_2: KartMission2
|
||||
@@ -1022,4 +1428,5 @@ class SA2BOptions(PerGameCommonOptions):
|
||||
cannons_core_mission_5: CannonsCoreMission5
|
||||
|
||||
ring_link: RingLink
|
||||
trap_link: TrapLink
|
||||
death_link: DeathLink
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from .Options import *
|
||||
|
||||
minsanity = {
|
||||
"goal": Goal.option_chaos_chao,
|
||||
"max_emblem_cap": MaximumEmblemCap.range_start,
|
||||
|
||||
"keysanity": False,
|
||||
"whistlesanity": Whistlesanity.option_none,
|
||||
"beetlesanity": False,
|
||||
"omosanity": False,
|
||||
"animalsanity": False,
|
||||
"itemboxsanity": ItemBoxsanity.option_none,
|
||||
"bigsanity": False,
|
||||
"kart_race_checks": KartRaceChecks.option_none,
|
||||
|
||||
"junk_fill_percentage": 0,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_start,
|
||||
"sonic_mission_2": False,
|
||||
"sonic_mission_3": False,
|
||||
"sonic_mission_4": False,
|
||||
"sonic_mission_5": False,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_start,
|
||||
"shadow_mission_2": False,
|
||||
"shadow_mission_3": False,
|
||||
"shadow_mission_4": False,
|
||||
"shadow_mission_5": False,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_start,
|
||||
"tails_mission_2": False,
|
||||
"tails_mission_3": False,
|
||||
"tails_mission_4": False,
|
||||
"tails_mission_5": False,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_start,
|
||||
"eggman_mission_2": False,
|
||||
"eggman_mission_3": False,
|
||||
"eggman_mission_4": False,
|
||||
"eggman_mission_5": False,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_start,
|
||||
"knuckles_mission_2": False,
|
||||
"knuckles_mission_3": False,
|
||||
"knuckles_mission_4": False,
|
||||
"knuckles_mission_5": False,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_start,
|
||||
"rouge_mission_2": False,
|
||||
"rouge_mission_3": False,
|
||||
"rouge_mission_4": False,
|
||||
"rouge_mission_5": False,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_start,
|
||||
"kart_mission_2": False,
|
||||
"kart_mission_3": False,
|
||||
"kart_mission_4": False,
|
||||
"kart_mission_5": False,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_start,
|
||||
"cannons_core_mission_2": False,
|
||||
"cannons_core_mission_3": False,
|
||||
"cannons_core_mission_4": False,
|
||||
"cannons_core_mission_5": False,
|
||||
}
|
||||
|
||||
chao_centric = {
|
||||
"goal": Goal.option_chaos_chao,
|
||||
|
||||
"keysanity": False,
|
||||
"whistlesanity": Whistlesanity.option_none,
|
||||
"beetlesanity": False,
|
||||
"omosanity": False,
|
||||
"animalsanity": False,
|
||||
"itemboxsanity": ItemBoxsanity.option_none,
|
||||
"bigsanity": False,
|
||||
"kart_race_checks": KartRaceChecks.option_none,
|
||||
|
||||
"black_market_slots": BlackMarketSlots.range_end,
|
||||
"black_market_unlock_costs": BlackMarketUnlockCosts.option_high,
|
||||
"chao_race_difficulty": ChaoRaceDifficulty.option_expert,
|
||||
"chao_karate_difficulty": ChaoKarateDifficulty.option_super,
|
||||
"chao_stadium_checks": ChaoStadiumChecks.option_all,
|
||||
"chao_animal_parts": True,
|
||||
"chao_stats": ChaoStats.range_end,
|
||||
"chao_stats_frequency": 1,
|
||||
"chao_stats_stamina": True,
|
||||
"chao_stats_hidden": True,
|
||||
"chao_kindergarten": ChaoKindergarten.option_full,
|
||||
|
||||
"junk_fill_percentage": 50,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_start,
|
||||
"sonic_mission_2": False,
|
||||
"sonic_mission_3": False,
|
||||
"sonic_mission_4": False,
|
||||
"sonic_mission_5": False,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_start,
|
||||
"shadow_mission_2": False,
|
||||
"shadow_mission_3": False,
|
||||
"shadow_mission_4": False,
|
||||
"shadow_mission_5": False,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_start,
|
||||
"tails_mission_2": False,
|
||||
"tails_mission_3": False,
|
||||
"tails_mission_4": False,
|
||||
"tails_mission_5": False,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_start,
|
||||
"eggman_mission_2": False,
|
||||
"eggman_mission_3": False,
|
||||
"eggman_mission_4": False,
|
||||
"eggman_mission_5": False,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_start,
|
||||
"knuckles_mission_2": False,
|
||||
"knuckles_mission_3": False,
|
||||
"knuckles_mission_4": False,
|
||||
"knuckles_mission_5": False,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_start,
|
||||
"rouge_mission_2": False,
|
||||
"rouge_mission_3": False,
|
||||
"rouge_mission_4": False,
|
||||
"rouge_mission_5": False,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_start,
|
||||
"kart_mission_2": False,
|
||||
"kart_mission_3": False,
|
||||
"kart_mission_4": False,
|
||||
"kart_mission_5": False,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_start,
|
||||
"cannons_core_mission_2": False,
|
||||
"cannons_core_mission_3": False,
|
||||
"cannons_core_mission_4": False,
|
||||
"cannons_core_mission_5": False,
|
||||
}
|
||||
|
||||
allsanity_no_chao = {
|
||||
"goal": Goal.option_cannons_core_boss_rush,
|
||||
"boss_rush_shuffle": BossRushShuffle.option_chaos,
|
||||
"minigame_madness_requirement": MinigameMadnessRequirement.range_end,
|
||||
"minigame_madness_minimum": MinigameMadnessMinimum.range_end,
|
||||
"max_emblem_cap": MaximumEmblemCap.range_end,
|
||||
|
||||
"mission_shuffle": True,
|
||||
"required_cannons_core_missions": RequiredCannonsCoreMissions.option_all_active,
|
||||
"emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore.range_end,
|
||||
"number_of_level_gates": NumberOfLevelGates.range_end,
|
||||
"level_gate_costs": LevelGateCosts.option_high,
|
||||
|
||||
"keysanity": True,
|
||||
"whistlesanity": Whistlesanity.option_both,
|
||||
"beetlesanity": True,
|
||||
"omosanity": True,
|
||||
"animalsanity": True,
|
||||
"itemboxsanity": ItemBoxsanity.option_all,
|
||||
"bigsanity": True,
|
||||
"kart_race_checks": KartRaceChecks.option_full,
|
||||
|
||||
"junk_fill_percentage": 25,
|
||||
"trap_fill_percentage": 25,
|
||||
"omochao_trap_weight": BaseTrapWeight.option_high,
|
||||
"timestop_trap_weight": BaseTrapWeight.option_high,
|
||||
"confusion_trap_weight": BaseTrapWeight.option_high,
|
||||
"tiny_trap_weight": BaseTrapWeight.option_high,
|
||||
"gravity_trap_weight": BaseTrapWeight.option_high,
|
||||
"exposition_trap_weight": BaseTrapWeight.option_high,
|
||||
"ice_trap_weight": BaseTrapWeight.option_high,
|
||||
"slow_trap_weight": BaseTrapWeight.option_high,
|
||||
"cutscene_trap_weight": BaseTrapWeight.option_high,
|
||||
"reverse_trap_weight": BaseTrapWeight.option_high,
|
||||
"literature_trap_weight": BaseTrapWeight.option_high,
|
||||
"controller_drift_trap_weight": BaseTrapWeight.option_high,
|
||||
"poison_trap_weight": BaseTrapWeight.option_high,
|
||||
"bee_trap_weight": BaseTrapWeight.option_high,
|
||||
"pong_trap_weight": BaseTrapWeight.option_high,
|
||||
"breakout_trap_weight": BaseTrapWeight.option_high,
|
||||
"fishing_trap_weight": BaseTrapWeight.option_high,
|
||||
"trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_count_trap_weight": BaseTrapWeight.option_high,
|
||||
"number_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"light_up_path_trap_weight": BaseTrapWeight.option_high,
|
||||
"pinball_trap_weight": BaseTrapWeight.option_high,
|
||||
"math_quiz_trap_weight": BaseTrapWeight.option_high,
|
||||
"snake_trap_weight": BaseTrapWeight.option_high,
|
||||
"input_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"minigame_trap_difficulty": MinigameTrapDifficulty.option_chaos,
|
||||
"big_fishing_difficulty": BigFishingDifficulty.option_chaos,
|
||||
|
||||
"music_shuffle": MusicShuffle.option_full,
|
||||
"voice_shuffle": VoiceShuffle.option_shuffled,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_end,
|
||||
"sonic_mission_2": True,
|
||||
"sonic_mission_3": True,
|
||||
"sonic_mission_4": True,
|
||||
"sonic_mission_5": True,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_end,
|
||||
"shadow_mission_2": True,
|
||||
"shadow_mission_3": True,
|
||||
"shadow_mission_4": True,
|
||||
"shadow_mission_5": True,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_end,
|
||||
"tails_mission_2": True,
|
||||
"tails_mission_3": True,
|
||||
"tails_mission_4": True,
|
||||
"tails_mission_5": True,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_end,
|
||||
"eggman_mission_2": True,
|
||||
"eggman_mission_3": True,
|
||||
"eggman_mission_4": True,
|
||||
"eggman_mission_5": True,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_end,
|
||||
"knuckles_mission_2": True,
|
||||
"knuckles_mission_3": True,
|
||||
"knuckles_mission_4": True,
|
||||
"knuckles_mission_5": True,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_end,
|
||||
"rouge_mission_2": True,
|
||||
"rouge_mission_3": True,
|
||||
"rouge_mission_4": True,
|
||||
"rouge_mission_5": True,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_end,
|
||||
"kart_mission_2": True,
|
||||
"kart_mission_3": True,
|
||||
"kart_mission_4": True,
|
||||
"kart_mission_5": True,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_end,
|
||||
"cannons_core_mission_2": True,
|
||||
"cannons_core_mission_3": True,
|
||||
"cannons_core_mission_4": True,
|
||||
"cannons_core_mission_5": True,
|
||||
}
|
||||
|
||||
allsanity = {
|
||||
"goal": Goal.option_cannons_core_boss_rush,
|
||||
"boss_rush_shuffle": BossRushShuffle.option_chaos,
|
||||
"minigame_madness_requirement": MinigameMadnessRequirement.range_end,
|
||||
"minigame_madness_minimum": MinigameMadnessMinimum.range_end,
|
||||
"max_emblem_cap": MaximumEmblemCap.range_end,
|
||||
|
||||
"mission_shuffle": True,
|
||||
"required_cannons_core_missions": RequiredCannonsCoreMissions.option_all_active,
|
||||
"emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore.range_end,
|
||||
"number_of_level_gates": NumberOfLevelGates.range_end,
|
||||
"level_gate_costs": LevelGateCosts.option_high,
|
||||
|
||||
"keysanity": True,
|
||||
"whistlesanity": Whistlesanity.option_both,
|
||||
"beetlesanity": True,
|
||||
"omosanity": True,
|
||||
"animalsanity": True,
|
||||
"itemboxsanity": ItemBoxsanity.option_all,
|
||||
"bigsanity": True,
|
||||
"kart_race_checks": KartRaceChecks.option_full,
|
||||
|
||||
"black_market_slots": BlackMarketSlots.range_end,
|
||||
"black_market_unlock_costs": BlackMarketUnlockCosts.option_high,
|
||||
"chao_race_difficulty": ChaoRaceDifficulty.option_expert,
|
||||
"chao_karate_difficulty": ChaoKarateDifficulty.option_super,
|
||||
"chao_stadium_checks": ChaoStadiumChecks.option_all,
|
||||
"chao_animal_parts": True,
|
||||
"chao_stats": ChaoStats.range_end,
|
||||
"chao_stats_frequency": 1,
|
||||
"chao_stats_stamina": True,
|
||||
"chao_stats_hidden": True,
|
||||
"chao_kindergarten": ChaoKindergarten.option_full,
|
||||
|
||||
"junk_fill_percentage": 25,
|
||||
"trap_fill_percentage": 25,
|
||||
"omochao_trap_weight": BaseTrapWeight.option_high,
|
||||
"timestop_trap_weight": BaseTrapWeight.option_high,
|
||||
"confusion_trap_weight": BaseTrapWeight.option_high,
|
||||
"tiny_trap_weight": BaseTrapWeight.option_high,
|
||||
"gravity_trap_weight": BaseTrapWeight.option_high,
|
||||
"exposition_trap_weight": BaseTrapWeight.option_high,
|
||||
"ice_trap_weight": BaseTrapWeight.option_high,
|
||||
"slow_trap_weight": BaseTrapWeight.option_high,
|
||||
"cutscene_trap_weight": BaseTrapWeight.option_high,
|
||||
"reverse_trap_weight": BaseTrapWeight.option_high,
|
||||
"literature_trap_weight": BaseTrapWeight.option_high,
|
||||
"controller_drift_trap_weight": BaseTrapWeight.option_high,
|
||||
"poison_trap_weight": BaseTrapWeight.option_high,
|
||||
"bee_trap_weight": BaseTrapWeight.option_high,
|
||||
"pong_trap_weight": BaseTrapWeight.option_high,
|
||||
"breakout_trap_weight": BaseTrapWeight.option_high,
|
||||
"fishing_trap_weight": BaseTrapWeight.option_high,
|
||||
"trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_count_trap_weight": BaseTrapWeight.option_high,
|
||||
"number_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"light_up_path_trap_weight": BaseTrapWeight.option_high,
|
||||
"pinball_trap_weight": BaseTrapWeight.option_high,
|
||||
"math_quiz_trap_weight": BaseTrapWeight.option_high,
|
||||
"snake_trap_weight": BaseTrapWeight.option_high,
|
||||
"input_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"minigame_trap_difficulty": MinigameTrapDifficulty.option_chaos,
|
||||
"big_fishing_difficulty": BigFishingDifficulty.option_chaos,
|
||||
|
||||
"music_shuffle": MusicShuffle.option_full,
|
||||
"voice_shuffle": VoiceShuffle.option_shuffled,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_end,
|
||||
"sonic_mission_2": True,
|
||||
"sonic_mission_3": True,
|
||||
"sonic_mission_4": True,
|
||||
"sonic_mission_5": True,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_end,
|
||||
"shadow_mission_2": True,
|
||||
"shadow_mission_3": True,
|
||||
"shadow_mission_4": True,
|
||||
"shadow_mission_5": True,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_end,
|
||||
"tails_mission_2": True,
|
||||
"tails_mission_3": True,
|
||||
"tails_mission_4": True,
|
||||
"tails_mission_5": True,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_end,
|
||||
"eggman_mission_2": True,
|
||||
"eggman_mission_3": True,
|
||||
"eggman_mission_4": True,
|
||||
"eggman_mission_5": True,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_end,
|
||||
"knuckles_mission_2": True,
|
||||
"knuckles_mission_3": True,
|
||||
"knuckles_mission_4": True,
|
||||
"knuckles_mission_5": True,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_end,
|
||||
"rouge_mission_2": True,
|
||||
"rouge_mission_3": True,
|
||||
"rouge_mission_4": True,
|
||||
"rouge_mission_5": True,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_end,
|
||||
"kart_mission_2": True,
|
||||
"kart_mission_3": True,
|
||||
"kart_mission_4": True,
|
||||
"kart_mission_5": True,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_end,
|
||||
"cannons_core_mission_2": True,
|
||||
"cannons_core_mission_3": True,
|
||||
"cannons_core_mission_4": True,
|
||||
"cannons_core_mission_5": True,
|
||||
}
|
||||
|
||||
all_random = {
|
||||
"goal": "random",
|
||||
"boss_rush_shuffle": "random",
|
||||
"minigame_madness_requirement": "random",
|
||||
"minigame_madness_minimum": "random",
|
||||
"logic_difficulty": "random",
|
||||
"required_rank": "random",
|
||||
"max_emblem_cap": "random",
|
||||
"ring_loss": "random",
|
||||
|
||||
"mission_shuffle": "random",
|
||||
"required_cannons_core_missions": "random",
|
||||
"emblem_percentage_for_cannons_core": "random",
|
||||
"number_of_level_gates": "random",
|
||||
"level_gate_distribution": "random",
|
||||
"level_gate_costs": "random",
|
||||
|
||||
"keysanity": "random",
|
||||
"whistlesanity": "random",
|
||||
"beetlesanity": "random",
|
||||
"omosanity": "random",
|
||||
"animalsanity": "random",
|
||||
"itemboxsanity": "random",
|
||||
"bigsanity": "random",
|
||||
"kart_race_checks": "random",
|
||||
|
||||
"black_market_slots": "random",
|
||||
"black_market_unlock_costs": "random",
|
||||
"black_market_price_multiplier": "random",
|
||||
"chao_race_difficulty": "random",
|
||||
"chao_karate_difficulty": "random",
|
||||
"chao_stadium_checks": "random",
|
||||
"chao_animal_parts": "random",
|
||||
"chao_stats": "random",
|
||||
"chao_stats_frequency": "random",
|
||||
"chao_stats_stamina": "random",
|
||||
"chao_stats_hidden": "random",
|
||||
"chao_kindergarten": "random",
|
||||
"shuffle_starting_chao_eggs": "random",
|
||||
"chao_entrance_randomization": "random",
|
||||
|
||||
"junk_fill_percentage": "random",
|
||||
"trap_fill_percentage": "random",
|
||||
"omochao_trap_weight": "random",
|
||||
"timestop_trap_weight": "random",
|
||||
"confusion_trap_weight": "random",
|
||||
"tiny_trap_weight": "random",
|
||||
"gravity_trap_weight": "random",
|
||||
"exposition_trap_weight": "random",
|
||||
"ice_trap_weight": "random",
|
||||
"slow_trap_weight": "random",
|
||||
"cutscene_trap_weight": "random",
|
||||
"reverse_trap_weight": "random",
|
||||
"literature_trap_weight": "random",
|
||||
"controller_drift_trap_weight": "random",
|
||||
"poison_trap_weight": "random",
|
||||
"bee_trap_weight": "random",
|
||||
"pong_trap_weight": "random",
|
||||
"breakout_trap_weight": "random",
|
||||
"fishing_trap_weight": "random",
|
||||
"trivia_trap_weight": "random",
|
||||
"pokemon_trivia_trap_weight": "random",
|
||||
"pokemon_count_trap_weight": "random",
|
||||
"number_sequence_trap_weight": "random",
|
||||
"light_up_path_trap_weight": "random",
|
||||
"pinball_trap_weight": "random",
|
||||
"math_quiz_trap_weight": "random",
|
||||
"snake_trap_weight": "random",
|
||||
"input_sequence_trap_weight": "random",
|
||||
"minigame_trap_difficulty": "random",
|
||||
"big_fishing_difficulty": "random",
|
||||
|
||||
"sadx_music": "random",
|
||||
"music_shuffle": "random",
|
||||
"voice_shuffle": "random",
|
||||
"narrator": "random",
|
||||
|
||||
"sonic_mission_count": "random",
|
||||
"sonic_mission_2": "random",
|
||||
"sonic_mission_3": "random",
|
||||
"sonic_mission_4": "random",
|
||||
"sonic_mission_5": "random",
|
||||
|
||||
"shadow_mission_count": "random",
|
||||
"shadow_mission_2": "random",
|
||||
"shadow_mission_3": "random",
|
||||
"shadow_mission_4": "random",
|
||||
"shadow_mission_5": "random",
|
||||
|
||||
"tails_mission_count": "random",
|
||||
"tails_mission_2": "random",
|
||||
"tails_mission_3": "random",
|
||||
"tails_mission_4": "random",
|
||||
"tails_mission_5": "random",
|
||||
|
||||
"eggman_mission_count": "random",
|
||||
"eggman_mission_2": "random",
|
||||
"eggman_mission_3": "random",
|
||||
"eggman_mission_4": "random",
|
||||
"eggman_mission_5": "random",
|
||||
|
||||
"knuckles_mission_count": "random",
|
||||
"knuckles_mission_2": "random",
|
||||
"knuckles_mission_3": "random",
|
||||
"knuckles_mission_4": "random",
|
||||
"knuckles_mission_5": "random",
|
||||
|
||||
"rouge_mission_count": "random",
|
||||
"rouge_mission_2": "random",
|
||||
"rouge_mission_3": "random",
|
||||
"rouge_mission_4": "random",
|
||||
"rouge_mission_5": "random",
|
||||
|
||||
"kart_mission_count": "random",
|
||||
"kart_mission_2": "random",
|
||||
"kart_mission_3": "random",
|
||||
"kart_mission_4": "random",
|
||||
"kart_mission_5": "random",
|
||||
|
||||
"cannons_core_mission_count": "random",
|
||||
"cannons_core_mission_2": "random",
|
||||
"cannons_core_mission_3": "random",
|
||||
"cannons_core_mission_4": "random",
|
||||
"cannons_core_mission_5": "random",
|
||||
|
||||
"ring_link": "random",
|
||||
"trap_link": "random",
|
||||
"death_link": "random",
|
||||
}
|
||||
|
||||
sa2b_options_presets: Dict[str, Dict[str, Any]] = {
|
||||
"Minsanity": minsanity,
|
||||
"Chao-centric": chao_centric,
|
||||
"Allsanity No Chao": allsanity_no_chao,
|
||||
"Allsanity": allsanity,
|
||||
"All Random": all_random,
|
||||
}
|
||||
+89
-1122
File diff suppressed because it is too large
Load Diff
+2102
-13
File diff suppressed because it is too large
Load Diff
+149
-28
@@ -8,12 +8,13 @@ from worlds.AutoWorld import WebWorld, World
|
||||
from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \
|
||||
all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits
|
||||
from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name
|
||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups, \
|
||||
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table
|
||||
from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_table
|
||||
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
|
||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, minigame_trap_table, item_groups, \
|
||||
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table, event_table
|
||||
from .Locations import SA2BLocation, all_locations, location_groups, setup_locations, chao_animal_event_location_table, black_market_location_table
|
||||
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions, print_mission_orders_to_spoiler
|
||||
from .Names import ItemName, LocationName
|
||||
from .Options import SA2BOptions, sa2b_option_groups
|
||||
from .Presets import sa2b_options_presets
|
||||
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
|
||||
gate_0_blacklist_regions
|
||||
from .Rules import set_rules
|
||||
@@ -33,6 +34,7 @@ class SA2BWeb(WebWorld):
|
||||
|
||||
tutorials = [setup_en]
|
||||
option_groups = sa2b_option_groups
|
||||
options_presets = sa2b_options_presets
|
||||
|
||||
|
||||
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
|
||||
@@ -60,11 +62,14 @@ class SA2BWorld(World):
|
||||
topology_present = False
|
||||
|
||||
item_name_groups = item_groups
|
||||
location_name_groups = location_groups
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = all_locations
|
||||
|
||||
location_table: typing.Dict[str, int]
|
||||
|
||||
shuffled_region_list: typing.List[int]
|
||||
levels_per_gate: typing.List[int]
|
||||
mission_map: typing.Dict[int, int]
|
||||
mission_count_map: typing.Dict[int, int]
|
||||
emblems_for_cannons_core: int
|
||||
@@ -78,7 +83,7 @@ class SA2BWorld(World):
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {
|
||||
"ModVersion": 203,
|
||||
"ModVersion": 204,
|
||||
"Goal": self.options.goal.value,
|
||||
"MusicMap": self.generate_music_data(),
|
||||
"VoiceMap": self.generate_voice_data(),
|
||||
@@ -89,14 +94,20 @@ class SA2BWorld(World):
|
||||
"MusicShuffle": self.options.music_shuffle.value,
|
||||
"Narrator": self.options.narrator.value,
|
||||
"MinigameTrapDifficulty": self.options.minigame_trap_difficulty.value,
|
||||
"BigFishingDifficulty": self.options.big_fishing_difficulty.value,
|
||||
"RingLoss": self.options.ring_loss.value,
|
||||
"RingLink": self.options.ring_link.value,
|
||||
"TrapLink": self.options.trap_link.value,
|
||||
"RequiredRank": self.options.required_rank.value,
|
||||
"MinigameMadnessAmount": self.options.minigame_madness_requirement.value,
|
||||
"LogicDifficulty": self.options.logic_difficulty.value,
|
||||
"ChaoKeys": self.options.keysanity.value,
|
||||
"Whistlesanity": self.options.whistlesanity.value,
|
||||
"GoldBeetles": self.options.beetlesanity.value,
|
||||
"OmochaoChecks": self.options.omosanity.value,
|
||||
"AnimalChecks": self.options.animalsanity.value,
|
||||
"ItemBoxChecks": self.options.itemboxsanity.value,
|
||||
"BigChecks": self.options.bigsanity.value,
|
||||
"KartRaceChecks": self.options.kart_race_checks.value,
|
||||
"ChaoStadiumChecks": self.options.chao_stadium_checks.value,
|
||||
"ChaoRaceDifficulty": self.options.chao_race_difficulty.value,
|
||||
@@ -122,6 +133,7 @@ class SA2BWorld(World):
|
||||
"GateCosts": self.gate_costs,
|
||||
"GateBosses": self.gate_bosses,
|
||||
"BossRushMap": self.boss_rush_map,
|
||||
"ActiveTraps": self.output_active_traps(),
|
||||
"PlayerNum": self.player,
|
||||
}
|
||||
|
||||
@@ -151,12 +163,42 @@ class SA2BWorld(World):
|
||||
|
||||
valid_trap_weights = self.options.exposition_trap_weight.value + \
|
||||
self.options.reverse_trap_weight.value + \
|
||||
self.options.pong_trap_weight.value
|
||||
self.options.literature_trap_weight.value + \
|
||||
self.options.controller_drift_trap_weight.value + \
|
||||
self.options.poison_trap_weight.value + \
|
||||
self.options.bee_trap_weight.value + \
|
||||
self.options.pong_trap_weight.value + \
|
||||
self.options.breakout_trap_weight.value + \
|
||||
self.options.fishing_trap_weight.value + \
|
||||
self.options.trivia_trap_weight.value + \
|
||||
self.options.pokemon_trivia_trap_weight.value + \
|
||||
self.options.pokemon_count_trap_weight.value + \
|
||||
self.options.number_sequence_trap_weight.value + \
|
||||
self.options.light_up_path_trap_weight.value + \
|
||||
self.options.pinball_trap_weight.value + \
|
||||
self.options.math_quiz_trap_weight.value + \
|
||||
self.options.snake_trap_weight.value + \
|
||||
self.options.input_sequence_trap_weight.value
|
||||
|
||||
if valid_trap_weights == 0:
|
||||
self.options.exposition_trap_weight.value = 4
|
||||
self.options.reverse_trap_weight.value = 4
|
||||
self.options.literature_trap_weight.value = 4
|
||||
self.options.controller_drift_trap_weight.value = 4
|
||||
self.options.poison_trap_weight.value = 4
|
||||
self.options.bee_trap_weight.value = 4
|
||||
self.options.pong_trap_weight.value = 4
|
||||
self.options.breakout_trap_weight.value = 4
|
||||
self.options.fishing_trap_weight.value = 4
|
||||
self.options.trivia_trap_weight.value = 4
|
||||
self.options.pokemon_trivia_trap_weight.value = 4
|
||||
self.options.pokemon_count_trap_weight.value = 4
|
||||
self.options.number_sequence_trap_weight.value = 4
|
||||
self.options.light_up_path_trap_weight.value = 4
|
||||
self.options.pinball_trap_weight.value = 4
|
||||
self.options.math_quiz_trap_weight.value = 4
|
||||
self.options.snake_trap_weight.value = 4
|
||||
self.options.input_sequence_trap_weight.value = 4
|
||||
|
||||
if self.options.kart_race_checks.value == 0:
|
||||
self.options.kart_race_checks.value = 2
|
||||
@@ -164,8 +206,8 @@ class SA2BWorld(World):
|
||||
self.gate_bosses = {}
|
||||
self.boss_rush_map = {}
|
||||
else:
|
||||
self.gate_bosses = get_gate_bosses(self.multiworld, self)
|
||||
self.boss_rush_map = get_boss_rush_bosses(self.multiworld, self)
|
||||
self.gate_bosses = get_gate_bosses(self)
|
||||
self.boss_rush_map = get_boss_rush_bosses(self)
|
||||
|
||||
def create_regions(self):
|
||||
self.mission_map = get_mission_table(self.multiworld, self, self.player)
|
||||
@@ -177,7 +219,7 @@ class SA2BWorld(World):
|
||||
# Not Generate Basic
|
||||
self.black_market_costs = dict()
|
||||
|
||||
if self.options.goal.value in [0, 2, 4, 5, 6]:
|
||||
if self.options.goal.value in [0, 2, 4, 5, 6, 8]:
|
||||
self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
elif self.options.goal.value == 1:
|
||||
self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
@@ -202,7 +244,7 @@ class SA2BWorld(World):
|
||||
if self.options.goal.value != 3:
|
||||
# Fill item pool with all required items
|
||||
for item in {**upgrades_table}:
|
||||
itempool += [self.create_item(item, False, self.options.goal.value)]
|
||||
itempool += [self.create_item(item, None, self.options.goal.value)]
|
||||
|
||||
if self.options.goal.value in [1, 2, 6]:
|
||||
# Some flavor of Chaos Emerald Hunt
|
||||
@@ -212,6 +254,25 @@ class SA2BWorld(World):
|
||||
# Black Market
|
||||
itempool += [self.create_item(ItemName.market_token) for _ in range(self.options.black_market_slots.value)]
|
||||
|
||||
if self.options.goal.value in [8]:
|
||||
available_locations: int = total_required_locations - len(itempool) - self.options.number_of_level_gates.value
|
||||
|
||||
while (self.options.minigame_madness_requirement.value * len(minigame_trap_table)) > available_locations:
|
||||
self.options.minigame_madness_requirement.value -= 1
|
||||
|
||||
while (self.options.minigame_madness_minimum.value * len(minigame_trap_table)) > available_locations:
|
||||
self.options.minigame_madness_minimum.value -= 1
|
||||
|
||||
traps_to_create: int = max(self.options.minigame_madness_minimum.value, self.options.minigame_madness_requirement.value)
|
||||
|
||||
# Minigame Madness
|
||||
for item in {**minigame_trap_table}:
|
||||
for i in range(traps_to_create):
|
||||
classification: ItemClassification = ItemClassification.trap
|
||||
if i < self.options.minigame_madness_requirement.value:
|
||||
classification |= ItemClassification.progression
|
||||
itempool.append(self.create_item(item, classification))
|
||||
|
||||
black_market_unlock_mult = 1.0
|
||||
if self.options.black_market_unlock_costs.value == 0:
|
||||
black_market_unlock_mult = 0.5
|
||||
@@ -235,12 +296,12 @@ class SA2BWorld(World):
|
||||
elif self.options.level_gate_costs.value == 1:
|
||||
gate_cost_mult = 0.8
|
||||
|
||||
shuffled_region_list = list(range(30))
|
||||
self.shuffled_region_list = list(range(30))
|
||||
emblem_requirement_list = list()
|
||||
self.multiworld.random.shuffle(shuffled_region_list)
|
||||
levels_per_gate = self.get_levels_per_gate()
|
||||
self.multiworld.random.shuffle(self.shuffled_region_list)
|
||||
self.levels_per_gate = self.get_levels_per_gate()
|
||||
|
||||
check_for_impossible_shuffle(shuffled_region_list, math.ceil(levels_per_gate[0]), self.multiworld)
|
||||
check_for_impossible_shuffle(self.shuffled_region_list, math.ceil(self.levels_per_gate[0]), self.multiworld)
|
||||
levels_added_to_gate = 0
|
||||
total_levels_added = 0
|
||||
current_gate = 0
|
||||
@@ -250,11 +311,11 @@ class SA2BWorld(World):
|
||||
gates = list()
|
||||
gates.append(LevelGate(0))
|
||||
for i in range(30):
|
||||
gates[current_gate].gate_levels.append(shuffled_region_list[i])
|
||||
gates[current_gate].gate_levels.append(self.shuffled_region_list[i])
|
||||
emblem_requirement_list.append(current_gate_emblems)
|
||||
levels_added_to_gate += 1
|
||||
total_levels_added += 1
|
||||
if levels_added_to_gate >= levels_per_gate[current_gate]:
|
||||
if levels_added_to_gate >= self.levels_per_gate[current_gate]:
|
||||
current_gate += 1
|
||||
if current_gate > self.options.number_of_level_gates.value:
|
||||
current_gate = self.options.number_of_level_gates.value
|
||||
@@ -265,18 +326,19 @@ class SA2BWorld(World):
|
||||
self.gate_costs[current_gate] = current_gate_emblems
|
||||
levels_added_to_gate = 0
|
||||
|
||||
self.region_emblem_map = dict(zip(shuffled_region_list, emblem_requirement_list))
|
||||
self.region_emblem_map = dict(zip(self.shuffled_region_list, emblem_requirement_list))
|
||||
|
||||
first_cannons_core_mission, final_cannons_core_mission = get_first_and_last_cannons_core_missions(self.mission_map, self.mission_count_map)
|
||||
|
||||
connect_regions(self.multiworld, self, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses, self.boss_rush_map, first_cannons_core_mission, final_cannons_core_mission)
|
||||
|
||||
max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core)
|
||||
max_required_emblems = min(int(max_required_emblems * 1.1), total_emblem_count)
|
||||
itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)]
|
||||
|
||||
non_required_emblems = (total_emblem_count - max_required_emblems)
|
||||
junk_count = math.floor(non_required_emblems * (self.options.junk_fill_percentage.value / 100.0))
|
||||
itempool += [self.create_item(ItemName.emblem, True) for _ in range(non_required_emblems - junk_count)]
|
||||
itempool += [self.create_item(ItemName.emblem, ItemClassification.filler) for _ in range(non_required_emblems - junk_count)]
|
||||
|
||||
# Carve Traps out of junk_count
|
||||
trap_weights = []
|
||||
@@ -291,7 +353,22 @@ class SA2BWorld(World):
|
||||
trap_weights += ([ItemName.slow_trap] * self.options.slow_trap_weight.value)
|
||||
trap_weights += ([ItemName.cutscene_trap] * self.options.cutscene_trap_weight.value)
|
||||
trap_weights += ([ItemName.reverse_trap] * self.options.reverse_trap_weight.value)
|
||||
trap_weights += ([ItemName.literature_trap] * self.options.literature_trap_weight.value)
|
||||
trap_weights += ([ItemName.controller_drift_trap] * self.options.controller_drift_trap_weight.value)
|
||||
trap_weights += ([ItemName.poison_trap] * self.options.poison_trap_weight.value)
|
||||
trap_weights += ([ItemName.bee_trap] * self.options.bee_trap_weight.value)
|
||||
trap_weights += ([ItemName.pong_trap] * self.options.pong_trap_weight.value)
|
||||
trap_weights += ([ItemName.breakout_trap] * self.options.breakout_trap_weight.value)
|
||||
trap_weights += ([ItemName.fishing_trap] * self.options.fishing_trap_weight.value)
|
||||
trap_weights += ([ItemName.trivia_trap] * self.options.trivia_trap_weight.value)
|
||||
trap_weights += ([ItemName.pokemon_trivia_trap] * self.options.pokemon_trivia_trap_weight.value)
|
||||
trap_weights += ([ItemName.pokemon_count_trap] * self.options.pokemon_count_trap_weight.value)
|
||||
trap_weights += ([ItemName.number_sequence_trap] * self.options.number_sequence_trap_weight.value)
|
||||
trap_weights += ([ItemName.light_up_path_trap] * self.options.light_up_path_trap_weight.value)
|
||||
trap_weights += ([ItemName.pinball_trap] * self.options.pinball_trap_weight.value)
|
||||
trap_weights += ([ItemName.math_quiz_trap] * self.options.math_quiz_trap_weight.value)
|
||||
trap_weights += ([ItemName.snake_trap] * self.options.snake_trap_weight.value)
|
||||
trap_weights += ([ItemName.input_sequence_trap] * self.options.input_sequence_trap_weight.value)
|
||||
|
||||
junk_count += extra_junk_count
|
||||
trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.options.trap_fill_percentage.value / 100.0))
|
||||
@@ -347,11 +424,15 @@ class SA2BWorld(World):
|
||||
|
||||
|
||||
|
||||
def create_item(self, name: str, force_non_progression=False, goal=0) -> Item:
|
||||
data = item_table[name]
|
||||
def create_item(self, name: str, force_classification=None, goal=0) -> Item:
|
||||
data = None
|
||||
if name in event_table:
|
||||
data = event_table[name]
|
||||
else:
|
||||
data = item_table[name]
|
||||
|
||||
if force_non_progression:
|
||||
classification = ItemClassification.filler
|
||||
if force_classification is not None:
|
||||
classification = force_classification
|
||||
elif name == ItemName.emblem or \
|
||||
name in emeralds_table.keys() or \
|
||||
(name == ItemName.knuckles_shovel_claws and goal in [4, 5]):
|
||||
@@ -380,9 +461,16 @@ class SA2BWorld(World):
|
||||
set_rules(self.multiworld, self, self.player, self.gate_bosses, self.boss_rush_map, self.mission_map, self.mission_count_map, self.black_market_costs)
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO):
|
||||
print_mission_orders_to_spoiler(self.mission_map,
|
||||
self.mission_count_map,
|
||||
self.shuffled_region_list,
|
||||
self.levels_per_gate,
|
||||
self.multiworld.player_name[self.player],
|
||||
spoiler_handle)
|
||||
|
||||
if self.options.number_of_level_gates.value > 0 or self.options.goal.value in [4, 5, 6]:
|
||||
spoiler_handle.write("\n")
|
||||
header_text = "Sonic Adventure 2 Bosses for {}:\n"
|
||||
header_text = "SA2 Bosses for {}:\n"
|
||||
header_text = header_text.format(self.multiworld.player_name[self.player])
|
||||
spoiler_handle.write(header_text)
|
||||
|
||||
@@ -435,20 +523,20 @@ class SA2BWorld(World):
|
||||
continue
|
||||
level_region = exit.connected_region
|
||||
for location in level_region.locations:
|
||||
er_hint_data[location.address] = gate_name
|
||||
if location.address != None:
|
||||
er_hint_data[location.address] = gate_name
|
||||
|
||||
for i in range(self.options.black_market_slots.value):
|
||||
location = self.multiworld.get_location(LocationName.chao_black_market_base + str(i + 1), self.player)
|
||||
er_hint_data[location.address] = str(self.black_market_costs[i]) + " " + str(ItemName.market_token)
|
||||
|
||||
|
||||
hint_data[self.player] = er_hint_data
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if multiworld.get_game_players("Sonic Adventure 2 Battle"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 0 if (item.name != 'Emblem') else 1)
|
||||
key=lambda item: 0 if ("Emblem" in item.name and item.game == "Sonic Adventure 2 Battle") else 1)
|
||||
|
||||
def get_levels_per_gate(self) -> list:
|
||||
levels_per_gate = list()
|
||||
@@ -486,6 +574,39 @@ class SA2BWorld(World):
|
||||
|
||||
return levels_per_gate
|
||||
|
||||
def output_active_traps(self) -> typing.Dict[int, int]:
|
||||
trap_data = {}
|
||||
|
||||
trap_data[0x30] = self.options.omochao_trap_weight.value
|
||||
trap_data[0x31] = self.options.timestop_trap_weight.value
|
||||
trap_data[0x32] = self.options.confusion_trap_weight.value
|
||||
trap_data[0x33] = self.options.tiny_trap_weight.value
|
||||
trap_data[0x34] = self.options.gravity_trap_weight.value
|
||||
trap_data[0x35] = self.options.exposition_trap_weight.value
|
||||
trap_data[0x37] = self.options.ice_trap_weight.value
|
||||
trap_data[0x38] = self.options.slow_trap_weight.value
|
||||
trap_data[0x39] = self.options.cutscene_trap_weight.value
|
||||
trap_data[0x3A] = self.options.reverse_trap_weight.value
|
||||
trap_data[0x3B] = self.options.literature_trap_weight.value
|
||||
trap_data[0x3C] = self.options.controller_drift_trap_weight.value
|
||||
trap_data[0x3D] = self.options.poison_trap_weight.value
|
||||
trap_data[0x3E] = self.options.bee_trap_weight.value
|
||||
|
||||
trap_data[0x50] = self.options.pong_trap_weight.value
|
||||
trap_data[0x51] = self.options.breakout_trap_weight.value
|
||||
trap_data[0x52] = self.options.fishing_trap_weight.value
|
||||
trap_data[0x53] = self.options.trivia_trap_weight.value
|
||||
trap_data[0x54] = self.options.pokemon_trivia_trap_weight.value
|
||||
trap_data[0x55] = self.options.pokemon_count_trap_weight.value
|
||||
trap_data[0x56] = self.options.number_sequence_trap_weight.value
|
||||
trap_data[0x57] = self.options.light_up_path_trap_weight.value
|
||||
trap_data[0x58] = self.options.pinball_trap_weight.value
|
||||
trap_data[0x59] = self.options.math_quiz_trap_weight.value
|
||||
trap_data[0x5A] = self.options.snake_trap_weight.value
|
||||
trap_data[0x5B] = self.options.input_sequence_trap_weight.value
|
||||
|
||||
return trap_data
|
||||
|
||||
def any_chao_locations_active(self) -> bool:
|
||||
if self.options.chao_race_difficulty.value > 0 or \
|
||||
self.options.chao_karate_difficulty.value > 0 or \
|
||||
@@ -686,7 +807,6 @@ class SA2BWorld(World):
|
||||
exit_choice = self.random.choice(valid_kindergarten_exits)
|
||||
exit_room = exit_to_room_map[exit_choice]
|
||||
all_exits_copy.remove(exit_choice)
|
||||
multi_rooms_copy.remove(exit_room)
|
||||
|
||||
destination = 0x06
|
||||
single_rooms_copy.remove(destination)
|
||||
@@ -723,7 +843,8 @@ class SA2BWorld(World):
|
||||
|
||||
er_layout[exit_choice] = destination
|
||||
|
||||
reverse_exit = self.random.choice(room_to_exits_map[destination])
|
||||
possible_reverse_exits = [exit for exit in room_to_exits_map[destination] if exit in all_exits_copy]
|
||||
reverse_exit = self.random.choice(possible_reverse_exits)
|
||||
|
||||
er_layout[reverse_exit] = exit_room
|
||||
|
||||
|
||||
@@ -1625,6 +1625,6 @@ def get_location_offset(mission_id):
|
||||
|
||||
|
||||
def launch():
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# StarCraft 2
|
||||
|
||||
## Game page in other languages:
|
||||
|
||||
* [Français](/games/Starcraft%202/info/fr)
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
### Items and locations
|
||||
|
||||
The following unlocks are randomized as items:
|
||||
1. Your ability to build any non-worker unit.
|
||||
2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain
|
||||
@@ -34,18 +37,28 @@ When you receive items, they will immediately become available, even during a mi
|
||||
notified via a text box in the top-right corner of the game screen.
|
||||
Item unlocks are also logged in the Archipelago client.
|
||||
|
||||
### Mission order
|
||||
|
||||
The missions and the order in which they need to be completed, referred to as the mission order, can also be randomized.
|
||||
The four StarCraft 2 campaigns can be used to populate the mission order.
|
||||
Note that the evolution missions from Heart of the Swarm are not included in the randomizer.
|
||||
The default mission order follows the structure of the selected campaigns but several other options are available,
|
||||
e.g., blitz, grid, etc.
|
||||
|
||||
Missions are launched through the StarCraft 2 Archipelago client, through the StarCraft 2 Launcher tab.
|
||||
The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included.
|
||||
Additionally, metaprogression currencies such as credits and Solarite are not used.
|
||||
Available missions are in blue; missions where all locations were collected are in white.
|
||||
If you move your mouse over a mission, the uncollected locations will be displayed, categorized by type.
|
||||
Unavailable missions are in grey; their requirements will also be shown there.
|
||||
|
||||
## What is the goal of this game when randomized?
|
||||
|
||||
The goal is to beat the final mission in the mission order.
|
||||
The yaml configuration file controls the mission order (e.g. blitz, grid, etc.), which combination of the four
|
||||
StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled.
|
||||
The yaml configuration file controls the mission order, which combination of the four StarCraft 2 campaigns can be
|
||||
used, and how missions are shuffled.
|
||||
Since the first two options determine the number of missions in a StarCraft 2 world, they can be used to customize the
|
||||
expected time to complete the world.
|
||||
Note that the evolution missions from Heart of the Swarm are not included in the randomizer.
|
||||
|
||||
## What non-randomized changes are there from vanilla StarCraft 2?
|
||||
|
||||
@@ -78,9 +91,7 @@ Will overwrite existing files
|
||||
* `/game_speed [game_speed]` Overrides the game speed for the world
|
||||
* Options: default, slower, slow, normal, fast, faster
|
||||
* `/color [faction] [color]` Changes your color for one of your playable factions.
|
||||
* Faction options: raynor, kerrigan, primal, protoss, nova
|
||||
* Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen,
|
||||
brown, lightgreen, darkgrey, pink, rainbow, random, default
|
||||
* Run without arguments to list all factions and colors that are available.
|
||||
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
|
||||
* Run without arguments to list all options.
|
||||
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource
|
||||
@@ -100,6 +111,19 @@ Additionally, upgrades are grouped beneath their corresponding units or building
|
||||
A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown.
|
||||
Every item whose name, race, or group name contains the provided parameter will be shown.
|
||||
|
||||
## Particularities in a multiworld
|
||||
|
||||
### Collect on goal completion
|
||||
|
||||
One of the default options of multiworlds is that once a world has achieved its goal, it collects its items from all
|
||||
other worlds.
|
||||
If you do not want this to happen, you should ask the person generating the multiworld to set the `Collect Permission`
|
||||
option to something else, e.g., manual.
|
||||
If the generation is not done via the website, the person that does the generation should modify the `collect_mode`
|
||||
option in their `host.yaml` file prior to generation.
|
||||
If the multiworld has already been generated, the host can use the command `/option collect_mode [value]` to change
|
||||
this option.
|
||||
|
||||
## Known issues
|
||||
|
||||
- StarCraft 2 Archipelago does not support loading a saved game.
|
||||
@@ -108,3 +132,7 @@ For this reason, it is recommended to play on a difficulty level lower than what
|
||||
To restart a mission, use the StarCraft 2 Client.
|
||||
- A crash report is often generated when a mission is closed.
|
||||
This does not affect the game and can be ignored.
|
||||
- Currently, the StarCraft 2 client uses the Victory locations to determine which missions have been completed.
|
||||
As a result, the Archipelago collect feature can sometime grant access to missions that are connected to a mission that
|
||||
you did not complete.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## Quel est l'effet de la *randomization* sur ce jeu ?
|
||||
|
||||
### *Items* et *locations*
|
||||
|
||||
Les éléments qui suivent sont les *items* qui sont *randomized* et qui doivent être débloqués pour être utilisés dans
|
||||
le jeu:
|
||||
1. La capacité de produire des unités, excepté les drones/probes/scv.
|
||||
@@ -37,21 +39,33 @@ Quand vous recevez un *item*, il devient immédiatement disponible, même pendan
|
||||
la boîte de texte situé dans le coin en haut à droite de *StarCraft 2*.
|
||||
L'acquisition d'un *item* est aussi indiquée dans le client d'Archipelago.
|
||||
|
||||
### *Mission order*
|
||||
|
||||
Les missions et l'ordre dans lequel elles doivent être complétées, dénoté *mission order*, peuvent également être
|
||||
*randomized*.
|
||||
Les quatre campagnes de *StarCraft 2* peuvent être utilisées pour remplir le *mission order*.
|
||||
Notez que les missions d'évolution de *Heart of the Swarm* ne sont pas incluses dans le *randomizer*.
|
||||
Par défaut, le *mission order* suit la structure des campagnes sélectionnées, mais plusieurs autres options sont
|
||||
disponibles, comme *blitz*, *grid*, etc.
|
||||
|
||||
Les missions peuvent être lancées par le client *StarCraft 2 Archipelago*, via l'interface graphique de l'onglet
|
||||
*StarCraft 2 Launcher*.
|
||||
Les segments qui se passent sur l'*Hyperion*, un Léviathan et la *Spear of Adun* ne sont pas inclus.
|
||||
De plus, les points de progression tels que les crédits ou la Solarite ne sont pas utilisés dans *StarCraft 2
|
||||
De plus, les points de progression, tels que les crédits ou la Solarite, ne sont pas utilisés dans *StarCraft 2
|
||||
Archipelago*.
|
||||
Les missions accessibles ont leur nom en bleu, tandis que celles où toutes les *locations* ont été collectées
|
||||
apparaissent en blanc.
|
||||
En plaçant votre souris sur une mission, les *locations* non collectées s’affichent, classées par catégorie.
|
||||
Les missions qui ne sont pas accessibles ont leur nom en gris et leurs prérequis seront également affichés à cet endroit.
|
||||
|
||||
|
||||
## Quel est le but de ce jeu quand il est *randomized*?
|
||||
|
||||
Le but est de réussir la mission finale du *mission order* (e.g. *blitz*, *grid*, etc.).
|
||||
Le fichier de configuration yaml permet de spécifier le *mission order*, lesquelles des quatre campagnes de
|
||||
*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le
|
||||
*mission order*.
|
||||
Le fichier de configuration yaml permet de spécifier le *mission order*, quelle combinaison des quatre campagnes de
|
||||
*StarCraft 2* peuvent être utilisée et comment les missions sont distribuées dans le *mission order*.
|
||||
Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles
|
||||
peuvent être utilisées pour moduler le temps nécessaire pour terminer le monde.
|
||||
Notez que les missions d'évolution de Heart of the Swarm ne sont pas incluses dans le *randomizer*.
|
||||
|
||||
## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2*
|
||||
|
||||
@@ -89,9 +103,7 @@ Les fichiers existants vont être écrasés.
|
||||
* `/game_speed [game_speed]` Remplace la vitesse du jeu pour le monde.
|
||||
* Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*.
|
||||
* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable.
|
||||
* Les options de *faction*: raynor, kerrigan, primal, protoss, nova.
|
||||
* Les options de couleur: *white*, *red*, *blue*, *teal*, *purple*, *yellow*, *orange*, *green*, *lightpink*,
|
||||
*violet*, *lightgrey*, *darkgreen*, *brown*, *lightgreen*, *darkgrey*, *pink*, *rainbow*, *random*, *default*.
|
||||
* Si la commande est lancée sans option, la liste des *factions* et des couleurs disponibles sera affichée.
|
||||
* `/option [option_name] [option_value]` Permet de changer un option normalement définit dans le *yaml*.
|
||||
* Si la commande est lancée sans option, la liste des options qui sont modifiables va être affichée.
|
||||
* Les options qui peuvent être changées avec cette commande incluent sauter les cinématiques automatiquement, la
|
||||
@@ -114,6 +126,19 @@ De plus, les améliorations sont regroupées sous leurs unités/bâtiments corre
|
||||
Un paramètre de filtrage peut aussi être fourni, e.g., `/received Thor`, pour limiter le nombre d'*items* affichés.
|
||||
Tous les *items* dont le nom, la race ou le nom de groupe contient le paramètre fourni seront affichés.
|
||||
|
||||
## Particularités dans un multiworld
|
||||
|
||||
### *Collect on goal completion*
|
||||
|
||||
L'une des options par défaut des *multiworlds* est qu'une fois qu'un monde a atteint son objectif final, il collecte
|
||||
tous ses *items*, incluant ceux dans les autres mondes.
|
||||
Si vous ne souhaitez pas que cela se produise, vous devez demander à la personne générant le *multiworld* de changer
|
||||
l'option *Collect Permission*.
|
||||
Si la génération n'est pas effectuée via le site web, la personne qui effectue la génération doit modifier l'option
|
||||
`collect_mode` dans son fichier *host.yaml* avant la génération.
|
||||
Si le *multiworld* a déjà été généré, l'hôte peut utiliser la commande `/option collect_mode [valeur]` pour modifier
|
||||
cette option.
|
||||
|
||||
## Problèmes connus
|
||||
|
||||
- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde.
|
||||
@@ -123,3 +148,7 @@ normalement à l'aise.
|
||||
Pour redémarrer une mission, utilisez le client de *StarCraft 2 Archipelago*.
|
||||
- Un rapport d'erreur est souvent généré lorsqu'une mission est fermée.
|
||||
Cela n'affecte pas le jeu et peut être ignoré.
|
||||
- Actuellement, le client de *StarCraft 2* utilise la *location* associée à la victoire d'une mission pour déterminer
|
||||
si celle-ci a été complétée.
|
||||
En conséquence, la fonctionnalité *collect* d'*Archipelago* peut rendre accessible des missions connectées à une
|
||||
mission que vous n'avez pas terminée.
|
||||
@@ -41,6 +41,7 @@ Remember the name you enter in the options page or in the yaml file, you'll need
|
||||
Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information.
|
||||
|
||||
### Common yaml questions
|
||||
|
||||
#### How do I know I set my yaml up correctly?
|
||||
|
||||
The simplest way to check is to use the website [validator](/check).
|
||||
|
||||
@@ -49,6 +49,7 @@ Si vous désirez des informations et/ou instructions générales sur l'utilisati
|
||||
veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml).
|
||||
|
||||
### Questions récurrentes à propos du fichier *yaml*
|
||||
|
||||
#### Comment est-ce que je sais que mon *yaml* est bien défini?
|
||||
|
||||
La manière la plus simple de valider votre *yaml* est d'utiliser le
|
||||
|
||||
@@ -214,67 +214,67 @@ class StardewValleyWorld(World):
|
||||
def setup_victory(self):
|
||||
if self.options.goal == Goal.option_community_center:
|
||||
self.create_event_location(location_table[GoalName.community_center],
|
||||
self.logic.bundle.can_complete_community_center,
|
||||
self.logic.goal.can_complete_community_center(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_grandpa_evaluation:
|
||||
self.create_event_location(location_table[GoalName.grandpa_evaluation],
|
||||
self.logic.can_finish_grandpa_evaluation(),
|
||||
self.logic.goal.can_finish_grandpa_evaluation(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_bottom_of_the_mines:
|
||||
self.create_event_location(location_table[GoalName.bottom_of_the_mines],
|
||||
True_(),
|
||||
self.logic.goal.can_complete_bottom_of_the_mines(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_cryptic_note:
|
||||
self.create_event_location(location_table[GoalName.cryptic_note],
|
||||
self.logic.quest.can_complete_quest("Cryptic Note"),
|
||||
self.logic.goal.can_complete_cryptic_note(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_master_angler:
|
||||
self.create_event_location(location_table[GoalName.master_angler],
|
||||
self.logic.fishing.can_catch_every_fish_for_fishsanity(),
|
||||
self.logic.goal.can_complete_master_angler(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_complete_collection:
|
||||
self.create_event_location(location_table[GoalName.complete_museum],
|
||||
self.logic.museum.can_complete_museum(),
|
||||
self.logic.goal.can_complete_complete_collection(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_full_house:
|
||||
self.create_event_location(location_table[GoalName.full_house],
|
||||
(self.logic.relationship.has_children(2) & self.logic.relationship.can_reproduce()),
|
||||
self.logic.goal.can_complete_full_house(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_greatest_walnut_hunter:
|
||||
self.create_event_location(location_table[GoalName.greatest_walnut_hunter],
|
||||
self.logic.walnut.has_walnut(130),
|
||||
self.logic.goal.can_complete_greatest_walnut_hunter(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_protector_of_the_valley:
|
||||
self.create_event_location(location_table[GoalName.protector_of_the_valley],
|
||||
self.logic.monster.can_complete_all_monster_slaying_goals(),
|
||||
self.logic.goal.can_complete_protector_of_the_valley(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_full_shipment:
|
||||
self.create_event_location(location_table[GoalName.full_shipment],
|
||||
self.logic.shipping.can_ship_everything_in_slot(self.get_all_location_names()),
|
||||
self.logic.goal.can_complete_full_shipment(self.get_all_location_names()),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_gourmet_chef:
|
||||
self.create_event_location(location_table[GoalName.gourmet_chef],
|
||||
self.logic.cooking.can_cook_everything,
|
||||
self.logic.goal.can_complete_gourmet_chef(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_craft_master:
|
||||
self.create_event_location(location_table[GoalName.craft_master],
|
||||
self.logic.crafting.can_craft_everything,
|
||||
self.logic.goal.can_complete_craft_master(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_legend:
|
||||
self.create_event_location(location_table[GoalName.legend],
|
||||
self.logic.money.can_have_earned_total(10_000_000),
|
||||
self.logic.goal.can_complete_legend(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_mystery_of_the_stardrops:
|
||||
self.create_event_location(location_table[GoalName.mystery_of_the_stardrops],
|
||||
self.logic.has_all_stardrops(),
|
||||
self.logic.goal.can_complete_mystery_of_the_stardrop(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_allsanity:
|
||||
self.create_event_location(location_table[GoalName.allsanity],
|
||||
HasProgressionPercent(self.player, 100),
|
||||
self.logic.goal.can_complete_allsanity(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_perfection:
|
||||
self.create_event_location(location_table[GoalName.perfection],
|
||||
HasProgressionPercent(self.player, 100),
|
||||
self.logic.goal.can_complete_perfection(),
|
||||
Event.victory)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(Event.victory, self.player)
|
||||
|
||||
@@ -928,6 +928,7 @@ id,name,classification,groups,mod_name
|
||||
10518,Aurora Vineyard Tablet,progression,,Stardew Valley Expanded
|
||||
10519,Scarlett's Job Offer,progression,,Stardew Valley Expanded
|
||||
10520,Morgan's Schooling,progression,,Stardew Valley Expanded
|
||||
10521,Aurora Vineyard Reclamation,progression,,Stardew Valley Expanded
|
||||
10601,Magic Elixir Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
|
||||
10602,Travel Core Recipe,progression,CRAFTSANITY,Magic
|
||||
10603,Haste Elixir Recipe,progression,CRAFTSANITY,Stardew Valley Expanded
|
||||
|
||||
|
@@ -41,7 +41,7 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
|
||||
if fishing is not None and content.features.skill_progression.is_progressive:
|
||||
early_forced.append(fishing.level_name)
|
||||
|
||||
if options.quest_locations >= 0:
|
||||
if options.quest_locations.has_story_quests():
|
||||
early_candidates.append(Wallet.magnifying_glass)
|
||||
|
||||
if options.special_order_locations & stardew_options.SpecialOrderLocations.option_board:
|
||||
|
||||
@@ -264,7 +264,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley
|
||||
|
||||
def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
||||
number_progressive_raccoons = 9
|
||||
if options.quest_locations < 0:
|
||||
if options.quest_locations.has_no_story_quests():
|
||||
number_progressive_raccoons = number_progressive_raccoons - 1
|
||||
|
||||
items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons)
|
||||
@@ -387,7 +387,7 @@ def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValle
|
||||
|
||||
|
||||
def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
||||
if options.quest_locations < 0:
|
||||
if options.quest_locations.has_no_story_quests():
|
||||
return
|
||||
# items.append(item_factory("Adventurer's Guild")) # Now unlocked always!
|
||||
items.append(item_factory(Wallet.club_card))
|
||||
@@ -698,7 +698,7 @@ def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewV
|
||||
if not exclude_ginger_island:
|
||||
items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island])
|
||||
|
||||
if options.quest_locations < 0:
|
||||
if options.quest_locations.has_no_story_quests():
|
||||
return
|
||||
|
||||
items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items])
|
||||
|
||||
@@ -191,7 +191,7 @@ def extend_cropsanity_locations(randomized_locations: List[LocationData], conten
|
||||
|
||||
|
||||
def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
||||
if options.quest_locations < 0:
|
||||
if options.quest_locations.has_no_story_quests():
|
||||
return
|
||||
|
||||
story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST]
|
||||
@@ -317,7 +317,7 @@ def extend_mandatory_locations(randomized_locations: List[LocationData], options
|
||||
|
||||
|
||||
def extend_situational_quest_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
|
||||
if options.quest_locations < 0:
|
||||
if options.quest_locations.has_no_story_quests():
|
||||
return
|
||||
if ModNames.distant_lands in options.mods:
|
||||
if ModNames.alecto in options.mods:
|
||||
|
||||
@@ -76,7 +76,7 @@ SkillLogicMixin, QuestLogicMixin]]):
|
||||
self.logic.region.can_reach_location("Complete Boiler Room"))
|
||||
|
||||
def can_access_raccoon_bundles(self) -> StardewRule:
|
||||
if self.options.quest_locations < 0:
|
||||
if self.options.quest_locations.has_no_story_quests():
|
||||
return self.logic.received(CommunityUpgrade.raccoon, 1) & self.logic.quest.can_complete_quest(Quest.giant_stump)
|
||||
|
||||
# 1 - Break the tree
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user