mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-27 13:23:22 -07:00
Compare commits
26 Commits
empty-deat
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4762865d0b | ||
|
|
9de49aa419 | ||
|
|
294a67a4b4 | ||
|
|
0e99888926 | ||
|
|
74cbf10930 | ||
|
|
08d2909b0e | ||
|
|
0949b11436 | ||
|
|
9cdffe7f63 | ||
|
|
8b2a883669 | ||
|
|
b7fc96100c | ||
|
|
63cbc00a40 | ||
|
|
57b94dba6f | ||
|
|
0dd188e108 | ||
|
|
bf8c840293 | ||
|
|
c0244f3018 | ||
|
|
8af8502202 | ||
|
|
42eaeb92f0 | ||
|
|
7f35eb8867 | ||
|
|
785569c40c | ||
|
|
a9eb70a881 | ||
|
|
5d3d0c8625 | ||
|
|
7e32feeea3 | ||
|
|
0d1935e757 | ||
|
|
9b3ee018e9 | ||
|
|
1de411ec89 | ||
|
|
3192799bbf |
1
.github/pyright-config.json
vendored
1
.github/pyright-config.json
vendored
@@ -2,6 +2,7 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"../BizHawkClient.py",
|
"../BizHawkClient.py",
|
||||||
"../Patch.py",
|
"../Patch.py",
|
||||||
|
"../test/param.py",
|
||||||
"../test/general/test_groups.py",
|
"../test/general/test_groups.py",
|
||||||
"../test/general/test_helpers.py",
|
"../test/general/test_helpers.py",
|
||||||
"../test/general/test_memory.py",
|
"../test/general/test_memory.py",
|
||||||
|
|||||||
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -36,9 +36,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ilammy/msvc-dev-cmd@v1
|
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||||
if: startsWith(matrix.os,'windows')
|
if: startsWith(matrix.os,'windows')
|
||||||
- uses: Bacondish2023/setup-googletest@v1
|
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
||||||
with:
|
with:
|
||||||
build-type: 'Release'
|
build-type: 'Release'
|
||||||
- name: Build tests
|
- name: Build tests
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -1128,7 +1128,7 @@ def run_as_textclient(*args):
|
|||||||
args = handle_url_arg(args, parser=parser)
|
args = handle_url_arg(args, parser=parser)
|
||||||
|
|
||||||
# use colorama to display colored text highlighting on windows
|
# use colorama to display colored text highlighting on windows
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
parser = get_base_parser()
|
parser = get_base_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
la_task = None
|
la_task = None
|
||||||
client = None
|
client = None
|
||||||
# TODO: does this need to re-read on reset?
|
# TODO: does this need to re-read on reset?
|
||||||
found_checks = []
|
found_checks = set()
|
||||||
last_resend = time.time()
|
last_resend = time.time()
|
||||||
|
|
||||||
magpie_enabled = False
|
magpie_enabled = False
|
||||||
@@ -559,10 +559,6 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
self.ui = LADXManager(self)
|
self.ui = LADXManager(self)
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
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]):
|
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||||
# Store the entrances we find on the server for future sessions
|
# Store the entrances we find on the server for future sessions
|
||||||
message = [{
|
message = [{
|
||||||
@@ -613,8 +609,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
self.client.pending_deathlink = True
|
self.client.pending_deathlink = True
|
||||||
|
|
||||||
def new_checks(self, item_ids, ladxr_ids):
|
def new_checks(self, item_ids, ladxr_ids):
|
||||||
self.found_checks += item_ids
|
self.found_checks.update(item_ids)
|
||||||
create_task_log_exception(self.send_checks())
|
create_task_log_exception(self.check_locations(self.found_checks))
|
||||||
if self.magpie_enabled:
|
if self.magpie_enabled:
|
||||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
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:
|
if self.last_resend + 5.0 < now:
|
||||||
self.last_resend = now
|
self.last_resend = now
|
||||||
await self.send_checks()
|
await self.check_locations(self.found_checks)
|
||||||
if self.magpie_enabled:
|
if self.magpie_enabled:
|
||||||
try:
|
try:
|
||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
@@ -803,6 +799,6 @@ async def main():
|
|||||||
await ctx.shutdown()
|
await ctx.shutdown()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
|
|||||||
from BaseClasses import ItemClassification
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
min_client_version = Version(0, 1, 6)
|
min_client_version = Version(0, 1, 6)
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
|
|
||||||
def remove_from_list(container, value):
|
def remove_from_list(container, value):
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -1579,6 +1579,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
|||||||
player_output = {
|
player_output = {
|
||||||
"Game": multiworld.game[player],
|
"Game": multiworld.game[player],
|
||||||
"Name": multiworld.get_player_name(player),
|
"Name": multiworld.get_player_name(player),
|
||||||
|
"ID": player,
|
||||||
}
|
}
|
||||||
output.append(player_output)
|
output.append(player_output)
|
||||||
for option_key, option in world.options_dataclass.type_hints.items():
|
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)
|
game_option_names.append(display_name)
|
||||||
|
|
||||||
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
|
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 = DictWriter(file, fields)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(output)
|
writer.writerows(output)
|
||||||
|
|||||||
@@ -735,6 +735,6 @@ async def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ def main():
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(_main())
|
asyncio.run(_main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -446,6 +446,6 @@ if __name__ == '__main__':
|
|||||||
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
||||||
|
|
||||||
args, rest = parser.parse_known_args()
|
args, rest = parser.parse_known_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -213,7 +213,7 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro RandomizeButton(option_name, option) %}
|
{% 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 }}">
|
<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" }} />
|
<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 %}
|
{% else %}
|
||||||
<div class="unsupported-option">
|
<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>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ if __name__ == '__main__':
|
|||||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
help='Path to a Archipelago Binary Patch file')
|
help='Path to a Archipelago Binary Patch file')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
## Running Tests
|
||||||
|
|
||||||
#### Using Pycharm
|
#### Using Pycharm
|
||||||
@@ -100,3 +132,11 @@ next to the run and debug buttons.
|
|||||||
#### Running Tests without Pycharm
|
#### Running Tests without Pycharm
|
||||||
|
|
||||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
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.
|
||||||
|
|||||||
@@ -358,6 +358,34 @@ def randomize_entrances(
|
|||||||
if on_connect:
|
if on_connect:
|
||||||
on_connect(er_state, placed_exits)
|
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:
|
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
|
||||||
nonlocal perform_validity_check
|
nonlocal perform_validity_check
|
||||||
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
|
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
|
||||||
@@ -371,11 +399,9 @@ def randomize_entrances(
|
|||||||
# very last exit and check whatever exits we open up are functionally accessible.
|
# 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.
|
# 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
|
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
|
||||||
or target_entrance.connected_region not in er_state.placed_regions)
|
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)
|
|
||||||
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
|
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)):
|
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
|
||||||
continue
|
continue
|
||||||
do_placement(source_exit, target_entrance)
|
do_placement(source_exit, target_entrance)
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class TestRandomizeEntrances(unittest.TestCase):
|
|||||||
self.assertEqual(80, len(result.pairings))
|
self.assertEqual(80, len(result.pairings))
|
||||||
self.assertEqual(80, len(result.placements))
|
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"""
|
"""tests that in coupled mode, all 2 way transitions have an inverse"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5)
|
generate_disconnected_region_grid(multiworld, 5)
|
||||||
@@ -236,6 +236,36 @@ class TestRandomizeEntrances(unittest.TestCase):
|
|||||||
# if we didn't visit every placement the verification on_connect doesn't really mean much
|
# if we didn't visit every placement the verification on_connect doesn't really mean much
|
||||||
self.assertEqual(len(result.placements), seen_placement_count)
|
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):
|
def test_uncoupled(self):
|
||||||
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from typing import List, Tuple
|
from typing import ClassVar, List, Tuple
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Location, MultiWorld
|
from BaseClasses import CollectionState, Location, MultiWorld
|
||||||
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
|
|||||||
from Options import Accessibility
|
from Options import Accessibility
|
||||||
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
||||||
from ..general import gen_steps, setup_multiworld
|
from ..general import gen_steps, setup_multiworld
|
||||||
|
from ..param import classvar_matrix
|
||||||
|
|
||||||
|
|
||||||
class MultiworldTestBase(TestCase):
|
class MultiworldTestBase(TestCase):
|
||||||
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
|||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||||
|
|
||||||
|
|
||||||
|
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
|
||||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||||
|
game: ClassVar[str]
|
||||||
|
|
||||||
def test_two_player_single_game_fills(self) -> None:
|
def test_two_player_single_game_fills(self) -> None:
|
||||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||||
for world_type in AutoWorldRegister.world_types.values():
|
world_type = AutoWorldRegister.world_types[self.game]
|
||||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_full
|
world.options.accessibility.value = Accessibility.option_full
|
||||||
self.assertSteps(gen_steps)
|
self.assertSteps(gen_steps)
|
||||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
call_all(self.multiworld, "post_fill")
|
call_all(self.multiworld, "post_fill")
|
||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||||
|
|||||||
46
test/param.py
Normal file
46
test/param.py
Normal file
@@ -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")
|
Utils.init_logging("BizHawkClient", exception_logger="Client")
|
||||||
import colorama
|
import colorama
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -261,6 +261,6 @@ def launch():
|
|||||||
# options = Utils.get_options()
|
# options = Utils.get_options()
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -515,10 +515,15 @@ def _populate_sprite_table():
|
|||||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
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):
|
for file in os.listdir(dir):
|
||||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
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():
|
class Sprite():
|
||||||
sprite_size = 28672
|
sprite_size = 28672
|
||||||
@@ -554,6 +559,11 @@ class Sprite():
|
|||||||
self.sprite = filedata[0x80000:0x87000]
|
self.sprite = filedata[0x80000:0x87000]
|
||||||
self.palette = filedata[0xDD308:0xDD380]
|
self.palette = filedata[0xDD308:0xDD380]
|
||||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
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'):
|
elif filedata.startswith(b'ZSPR'):
|
||||||
self.from_zspr(filedata, filename)
|
self.from_zspr(filedata, filename)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
# Celeste 64 - Changelog
|
# 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
|
## v1.2
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
|
|||||||
@@ -39,6 +39,22 @@ move_item_data_table: Dict[str, Celeste64ItemData] = {
|
|||||||
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
|
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}
|
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 typing import Dict, NamedTuple, Optional
|
||||||
|
|
||||||
from BaseClasses import Location
|
from BaseClasses import Location
|
||||||
from .Names import LocationName
|
from .Names import LocationName, RegionName
|
||||||
|
|
||||||
|
|
||||||
celeste_64_base_id: int = 0xCA0000
|
celeste_64_base_id: int = 0xCA0000
|
||||||
@@ -17,66 +17,80 @@ class Celeste64LocationData(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
|
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
|
LocationName.strawberry_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x00),
|
||||||
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
|
LocationName.strawberry_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x01),
|
||||||
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
|
LocationName.strawberry_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x02),
|
||||||
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
|
LocationName.strawberry_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x03),
|
||||||
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
|
LocationName.strawberry_5: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x04),
|
||||||
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
|
LocationName.strawberry_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x05),
|
||||||
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
|
LocationName.strawberry_7: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x06),
|
||||||
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
|
LocationName.strawberry_8: Celeste64LocationData(RegionName.nw_girders_island, celeste_64_base_id + 0x07),
|
||||||
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
|
LocationName.strawberry_9: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x08),
|
||||||
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
|
LocationName.strawberry_10: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x09),
|
||||||
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
|
LocationName.strawberry_11: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x0A),
|
||||||
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
|
LocationName.strawberry_12: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x0B),
|
||||||
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
|
LocationName.strawberry_13: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x0C),
|
||||||
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
|
LocationName.strawberry_14: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0D),
|
||||||
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
|
LocationName.strawberry_15: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0E),
|
||||||
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
|
LocationName.strawberry_16: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0F),
|
||||||
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
|
LocationName.strawberry_17: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x10),
|
||||||
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
|
LocationName.strawberry_18: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x11),
|
||||||
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
|
LocationName.strawberry_19: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x12),
|
||||||
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
|
LocationName.strawberry_20: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x13),
|
||||||
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
|
LocationName.strawberry_21: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x14),
|
||||||
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
|
LocationName.strawberry_22: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x15),
|
||||||
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
|
LocationName.strawberry_23: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x16),
|
||||||
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
|
LocationName.strawberry_24: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x17),
|
||||||
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
|
LocationName.strawberry_25: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x18),
|
||||||
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
|
LocationName.strawberry_26: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x19),
|
||||||
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
|
LocationName.strawberry_27: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1A),
|
||||||
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
|
LocationName.strawberry_28: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1B),
|
||||||
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
|
LocationName.strawberry_29: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x1C),
|
||||||
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
|
LocationName.strawberry_30: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x1D),
|
||||||
}
|
}
|
||||||
|
|
||||||
friend_location_data_table: Dict[str, Celeste64LocationData] = {
|
friend_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
|
LocationName.granny_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x00),
|
||||||
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
|
LocationName.granny_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x01),
|
||||||
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
|
LocationName.granny_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x02),
|
||||||
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
|
LocationName.theo_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x03),
|
||||||
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
|
LocationName.theo_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x04),
|
||||||
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
|
LocationName.theo_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x05),
|
||||||
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
|
LocationName.badeline_1: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x06),
|
||||||
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
|
LocationName.badeline_2: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x07),
|
||||||
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
|
LocationName.badeline_3: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x08),
|
||||||
}
|
}
|
||||||
|
|
||||||
sign_location_data_table: Dict[str, Celeste64LocationData] = {
|
sign_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
|
LocationName.sign_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x00),
|
||||||
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
|
LocationName.sign_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x01),
|
||||||
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
|
LocationName.sign_3: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x200 + 0x02),
|
||||||
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
|
LocationName.sign_4: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x200 + 0x03),
|
||||||
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
|
LocationName.sign_5: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x200 + 0x04),
|
||||||
}
|
}
|
||||||
|
|
||||||
car_location_data_table: Dict[str, Celeste64LocationData] = {
|
car_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
|
LocationName.car_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x300 + 0x00),
|
||||||
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
|
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,
|
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
|
||||||
**friend_location_data_table,
|
**friend_location_data_table,
|
||||||
**sign_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}
|
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"
|
air_dash = "Air Dash"
|
||||||
skid_jump = "Skid Jump"
|
skid_jump = "Skid Jump"
|
||||||
climb = "Climb"
|
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_9 = "South-West Dash Refills Strawberry"
|
||||||
strawberry_10 = "South-East Tower Side Strawberry"
|
strawberry_10 = "South-East Tower Side Strawberry"
|
||||||
strawberry_11 = "Girders Strawberry"
|
strawberry_11 = "Girders Strawberry"
|
||||||
strawberry_12 = "North-East Tower Bottom Strawberry"
|
strawberry_12 = "Badeline Tower Bottom Strawberry"
|
||||||
strawberry_13 = "Breakable Blocks Strawberry"
|
strawberry_13 = "Breakable Blocks Strawberry"
|
||||||
strawberry_14 = "Feather Maze Strawberry"
|
strawberry_14 = "Feather Maze Strawberry"
|
||||||
strawberry_15 = "Feather Chain Strawberry"
|
strawberry_15 = "Feather Chain Strawberry"
|
||||||
@@ -18,7 +18,7 @@ strawberry_16 = "Feather Hidden Strawberry"
|
|||||||
strawberry_17 = "Double Dash Puzzle Strawberry"
|
strawberry_17 = "Double Dash Puzzle Strawberry"
|
||||||
strawberry_18 = "Double Dash Spike Climb Strawberry"
|
strawberry_18 = "Double Dash Spike Climb Strawberry"
|
||||||
strawberry_19 = "Double Dash Spring 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_21 = "Theo Tower Lower Cassette Strawberry"
|
||||||
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
|
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
|
||||||
strawberry_23 = "South End of Bridge 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_26 = "North End of Bridge Cassette Strawberry"
|
||||||
strawberry_27 = "Distant Feather Cassette Strawberry"
|
strawberry_27 = "Distant Feather Cassette Strawberry"
|
||||||
strawberry_28 = "Feather Arches Cassette Strawberry"
|
strawberry_28 = "Feather Arches Cassette Strawberry"
|
||||||
strawberry_29 = "North-East Tower Cassette Strawberry"
|
strawberry_29 = "Badeline Tower Cassette Strawberry"
|
||||||
strawberry_30 = "Badeline Cassette Strawberry"
|
strawberry_30 = "Badeline Island Cassette Strawberry"
|
||||||
|
|
||||||
# Friend Locations
|
# Friend Locations
|
||||||
granny_1 = "Granny Conversation 1"
|
granny_1 = "Granny Conversation 1"
|
||||||
@@ -51,3 +51,15 @@ sign_5 = "Credits Sign"
|
|||||||
# Car Locations
|
# Car Locations
|
||||||
car_1 = "Intro Car"
|
car_1 = "Intro Car"
|
||||||
car_2 = "Secret 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"
|
||||||
|
|||||||
13
worlds/celeste64/Names/RegionName.py
Normal file
13
worlds/celeste64/Names/RegionName.py
Normal file
@@ -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"
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from dataclasses import dataclass
|
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):
|
class DeathLinkAmnesty(Range):
|
||||||
@@ -18,7 +20,7 @@ class TotalStrawberries(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Total Strawberries"
|
display_name = "Total Strawberries"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 46
|
range_end = 55
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
class StrawberriesRequiredPercentage(Range):
|
class StrawberriesRequiredPercentage(Range):
|
||||||
@@ -73,6 +75,93 @@ class Carsanity(Toggle):
|
|||||||
"""
|
"""
|
||||||
display_name = "Carsanity"
|
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):
|
class BadelineChaserSource(Choice):
|
||||||
"""
|
"""
|
||||||
@@ -119,6 +208,13 @@ celeste_64_option_groups = [
|
|||||||
Friendsanity,
|
Friendsanity,
|
||||||
Signsanity,
|
Signsanity,
|
||||||
Carsanity,
|
Carsanity,
|
||||||
|
Checkpointsanity,
|
||||||
|
]),
|
||||||
|
OptionGroup("Aesthetic Options", [
|
||||||
|
MadelineOneDashHairColor,
|
||||||
|
MadelineTwoDashHairColor,
|
||||||
|
MadelineNoDashHairColor,
|
||||||
|
MadelineFeatherHairColor,
|
||||||
]),
|
]),
|
||||||
OptionGroup("Badeline Chasers", [
|
OptionGroup("Badeline Chasers", [
|
||||||
BadelineChaserSource,
|
BadelineChaserSource,
|
||||||
@@ -142,7 +238,68 @@ class Celeste64Options(PerGameCommonOptions):
|
|||||||
friendsanity: Friendsanity
|
friendsanity: Friendsanity
|
||||||
signsanity: Signsanity
|
signsanity: Signsanity
|
||||||
carsanity: Carsanity
|
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_source: BadelineChaserSource
|
||||||
badeline_chaser_frequency: BadelineChaserFrequency
|
badeline_chaser_frequency: BadelineChaserFrequency
|
||||||
badeline_chaser_speed: BadelineChaserSpeed
|
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 typing import Dict, List, NamedTuple
|
||||||
|
|
||||||
|
from .Names import RegionName
|
||||||
|
|
||||||
class Celeste64RegionData(NamedTuple):
|
class Celeste64RegionData(NamedTuple):
|
||||||
connecting_regions: List[str] = []
|
connecting_regions: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
region_data_table: Dict[str, Celeste64RegionData] = {
|
region_data_table: Dict[str, Celeste64RegionData] = {
|
||||||
"Menu": Celeste64RegionData(["Forsaken City"]),
|
"Menu": Celeste64RegionData([RegionName.forsaken_city]),
|
||||||
"Forsaken City": Celeste64RegionData(),
|
|
||||||
|
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]),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 worlds.generic.Rules import set_rule
|
||||||
|
|
||||||
from . import Celeste64World
|
from . import Celeste64World
|
||||||
from .Names import ItemName, LocationName
|
from .Names import ItemName, LocationName, RegionName
|
||||||
|
|
||||||
|
|
||||||
def set_rules(world: Celeste64World):
|
def set_rules(world: Celeste64World):
|
||||||
if world.options.logic_difficulty == "standard":
|
if world.options.logic_difficulty == "standard":
|
||||||
if world.options.move_shuffle:
|
world.active_logic_mapping = location_standard_moves_logic
|
||||||
world.active_logic_mapping = location_standard_moves_logic
|
world.active_region_logic_mapping = region_standard_moves_logic
|
||||||
else:
|
|
||||||
world.active_logic_mapping = location_standard_logic
|
|
||||||
else:
|
else:
|
||||||
if world.options.move_shuffle:
|
world.active_logic_mapping = location_hard_moves_logic
|
||||||
world.active_logic_mapping = location_hard_moves_logic
|
world.active_region_logic_mapping = region_hard_moves_logic
|
||||||
else:
|
|
||||||
world.active_logic_mapping = location_hard_logic
|
|
||||||
|
|
||||||
for location in world.multiworld.get_locations(world.player):
|
for location in world.multiworld.get_locations(world.player):
|
||||||
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
|
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.
|
# Completion condition.
|
||||||
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
|
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]]] = {
|
location_standard_moves_logic: Dict[str, List[List[str]]] = {
|
||||||
LocationName.strawberry_1: [[ItemName.ground_dash],
|
LocationName.strawberry_1: [[ItemName.ground_dash],
|
||||||
[ItemName.air_dash],
|
[ItemName.air_dash],
|
||||||
[ItemName.skid_jump],
|
|
||||||
[ItemName.climb]],
|
|
||||||
LocationName.strawberry_2: [[ItemName.ground_dash],
|
|
||||||
[ItemName.air_dash],
|
|
||||||
[ItemName.skid_jump],
|
|
||||||
[ItemName.climb]],
|
[ItemName.climb]],
|
||||||
|
LocationName.strawberry_2: [[ItemName.air_dash],
|
||||||
|
[ItemName.skid_jump]],
|
||||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
LocationName.strawberry_3: [[ItemName.air_dash],
|
||||||
[ItemName.skid_jump]],
|
[ItemName.skid_jump]],
|
||||||
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||||
LocationName.strawberry_5: [[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_9: [[ItemName.dash_refill, ItemName.air_dash]],
|
||||||
LocationName.strawberry_10: [[ItemName.climb]],
|
LocationName.strawberry_10: [[ItemName.climb]],
|
||||||
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
LocationName.strawberry_11: [[ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.traffic_block, ItemName.climb]],
|
LocationName.strawberry_13: [[ItemName.breakables, ItemName.air_dash],
|
||||||
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
|
[ItemName.breakables, ItemName.ground_dash]],
|
||||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
|
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash]],
|
||||||
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
|
LocationName.strawberry_15: [[ItemName.feather, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
|
LocationName.strawberry_16: [[ItemName.feather]],
|
||||||
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
|
||||||
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
LocationName.strawberry_18: [[ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
|
LocationName.strawberry_19: [[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash, ItemName.skid_jump]],
|
||||||
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
|
LocationName.strawberry_20: [[ItemName.feather, ItemName.breakables, ItemName.air_dash]],
|
||||||
[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_21: [[ItemName.cassette, ItemName.traffic_block, 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_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],
|
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
|
||||||
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
|
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],
|
LocationName.strawberry_25: [[ItemName.cassette, 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.air_dash, ItemName.climb]],
|
||||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
LocationName.strawberry_27: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
|
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
|
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
|
||||||
[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_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.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_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||||
LocationName.theo_2: [[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.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],
|
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_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],
|
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash, ItemName.climb],
|
||||||
[ItemName.breakables, ItemName.air_dash]],
|
[ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||||
}
|
}
|
||||||
|
|
||||||
location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
||||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
|
||||||
[ItemName.skid_jump]],
|
|
||||||
LocationName.strawberry_5: [[ItemName.ground_dash],
|
LocationName.strawberry_5: [[ItemName.ground_dash],
|
||||||
[ItemName.air_dash]],
|
[ItemName.air_dash]],
|
||||||
LocationName.strawberry_8: [[ItemName.traffic_block],
|
|
||||||
[ItemName.ground_dash, ItemName.air_dash]],
|
|
||||||
LocationName.strawberry_10: [[ItemName.air_dash],
|
LocationName.strawberry_10: [[ItemName.air_dash],
|
||||||
[ItemName.climb]],
|
[ItemName.climb]],
|
||||||
LocationName.strawberry_11: [[ItemName.ground_dash],
|
LocationName.strawberry_11: [[ItemName.ground_dash],
|
||||||
[ItemName.air_dash],
|
[ItemName.air_dash],
|
||||||
[ItemName.skid_jump]],
|
[ItemName.skid_jump]],
|
||||||
LocationName.strawberry_12: [[ItemName.feather],
|
|
||||||
[ItemName.ground_dash],
|
|
||||||
[ItemName.air_dash]],
|
|
||||||
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
|
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_dash]],
|
[ItemName.breakables, ItemName.air_dash]],
|
||||||
LocationName.strawberry_14: [[ItemName.feather, 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],
|
LocationName.strawberry_15: [[ItemName.feather],
|
||||||
[ItemName.ground_dash, ItemName.air_dash]],
|
[ItemName.ground_dash, ItemName.air_dash]],
|
||||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
|
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]],
|
[ItemName.cassette, ItemName.feather, ItemName.climb]],
|
||||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
|
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
|
||||||
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
|
[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],
|
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.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.air_dash, ItemName.climb]],
|
||||||
[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.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_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],
|
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_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:
|
if loc not in world.active_logic_mapping:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -332,12 +204,28 @@ def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bo
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
|
def region_connection_rule(state: CollectionState, world: Celeste64World, region_connection: Tuple[str]) -> bool:
|
||||||
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
|
if region_connection not in world.active_region_logic_mapping:
|
||||||
return False
|
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):
|
if state.has_all(possible_access, world.player):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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 copy import deepcopy
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
from BaseClasses import ItemClassification, Location, Region, Tutorial
|
from BaseClasses import ItemClassification, Location, Region, Tutorial
|
||||||
from worlds.AutoWorld import WebWorld, World
|
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,\
|
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 .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):
|
class Celeste64WebWorld(WebWorld):
|
||||||
@@ -42,8 +44,15 @@ class Celeste64World(World):
|
|||||||
# Instance Data
|
# Instance Data
|
||||||
strawberries_required: int
|
strawberries_required: int
|
||||||
active_logic_mapping: Dict[str, List[List[str]]]
|
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:
|
def create_item(self, name: str) -> Celeste64Item:
|
||||||
# Only make required amount of strawberries be Progression
|
# Only make required amount of strawberries be Progression
|
||||||
@@ -76,25 +85,49 @@ class Celeste64World(World):
|
|||||||
for name in unlockable_item_data_table.keys()
|
for name in unlockable_item_data_table.keys()
|
||||||
if name not in self.options.start_inventory]
|
if name not in self.options.start_inventory]
|
||||||
|
|
||||||
if self.options.move_shuffle:
|
chosen_start_item: str = ""
|
||||||
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
|
|
||||||
|
|
||||||
|
if self.options.move_shuffle:
|
||||||
if self.options.logic_difficulty == "standard":
|
if self.options.logic_difficulty == "standard":
|
||||||
# If the start_inventory already includes a move, don't worry about giving it one
|
possible_unwalls: List[str] = [name for name in move_item_data_table.keys()
|
||||||
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
|
if name != ItemName.skid_jump]
|
||||||
chosen_start_move = self.random.choice(move_items_for_itempool)
|
|
||||||
move_items_for_itempool.remove(chosen_start_move)
|
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:
|
if self.options.carsanity:
|
||||||
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
|
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
|
location_count -= 1
|
||||||
else:
|
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)
|
item_pool += [self.create_item(name)
|
||||||
for name in move_items_for_itempool
|
for name in move_item_data_table.keys()
|
||||||
if name not in self.options.start_inventory]
|
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))
|
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))
|
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
|
if location_data.region == region_name
|
||||||
}, Celeste64Location)
|
}, 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:
|
def get_filler_item_name(self) -> str:
|
||||||
return ItemName.raspberry
|
return ItemName.raspberry
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
|
||||||
from .Rules import set_rules
|
|
||||||
set_rules(self)
|
|
||||||
|
|
||||||
|
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
return {
|
return {
|
||||||
"death_link": self.options.death_link.value,
|
"death_link": self.options.death_link.value,
|
||||||
@@ -161,6 +199,11 @@ class Celeste64World(World):
|
|||||||
"friendsanity": self.options.friendsanity.value,
|
"friendsanity": self.options.friendsanity.value,
|
||||||
"signsanity": self.options.signsanity.value,
|
"signsanity": self.options.signsanity.value,
|
||||||
"carsanity": self.options.carsanity.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_source": self.options.badeline_chaser_source.value,
|
||||||
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
|
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
|
||||||
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
|
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||||
import zipfile
|
import zipfile
|
||||||
from BaseClasses import Location
|
from BaseClasses import Location
|
||||||
from worlds.Files import APContainer
|
from worlds.Files import APContainer, AutoPatchRegister
|
||||||
|
|
||||||
from .Enum import CivVICheckType
|
from .Enum import CivVICheckType
|
||||||
from .Locations import CivVILocation, CivVILocationData
|
from .Locations import CivVILocation, CivVILocationData
|
||||||
@@ -25,18 +26,22 @@ class CivTreeItem:
|
|||||||
ui_tree_row: int
|
ui_tree_row: int
|
||||||
|
|
||||||
|
|
||||||
class CivVIContainer(APContainer):
|
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||||
"""
|
"""
|
||||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||||
"""
|
"""
|
||||||
game: Optional[str] = "Civilization VI"
|
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 = ""):
|
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||||
self.patch_data = patch_data
|
if isinstance(patch_data, io.BytesIO):
|
||||||
self.file_path = base_path
|
super().__init__(patch_data, player, player_name, server)
|
||||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
else:
|
||||||
super().__init__(container_path, player, player_name, server)
|
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:
|
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||||
for filename, yml in self.patch_data.items():
|
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, \
|
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
||||||
get_start_inventory_data
|
get_start_inventory_data
|
||||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_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
|
# CVCOTM_VC_US_HASH
|
||||||
from .client import CastlevaniaCotMClient
|
from .client import CastlevaniaCotMClient
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ class CVCotMSettings(settings.Group):
|
|||||||
"""File name of the Castlevania CotM US rom"""
|
"""File name of the Castlevania CotM US rom"""
|
||||||
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
||||||
description = "Castlevania CotM (US) ROM File"
|
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)
|
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
|
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.
|
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.
|
The Wii U Virtual Console version does not work due to changes in the code in that version.
|
||||||
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 is currently untested. If you happen to have purchased it before the Wii U eShop shut down, you can try
|
Due to the reasons mentioned above, it is most recommended to obtain the ROM by dumping it from an original cartridge of the
|
||||||
dumping and playing with it. However, at the moment, we cannot guarantee that it will work well due to it being untested.
|
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.
|
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).
|
- [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.
|
- 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.
|
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later.
|
||||||
|
|
||||||
### Configuring BizHawk
|
### Configuring BizHawk
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
|
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
|
||||||
CVCOTM_AC_US_HASH = "87a1bd6577b6702f97a60fc55772ad74" # Castlevania Advance Collection 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
|
# The Wii U VC version is not currently supported. See the Game Page for more info.
|
||||||
# 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.
|
|
||||||
|
|
||||||
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
|
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
|
||||||
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
|
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
|
||||||
@@ -518,7 +516,8 @@ class CVCotMPatchExtensions(APPatchExtension):
|
|||||||
|
|
||||||
|
|
||||||
class CVCotMProcedurePatch(APProcedurePatch, APTokenMixin):
|
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"
|
patch_file_ending: str = ".apcvcotm"
|
||||||
result_file_ending: str = ".gba"
|
result_file_ending: str = ".gba"
|
||||||
|
|
||||||
@@ -585,7 +584,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
|
|
||||||
basemd5 = hashlib.md5()
|
basemd5 = hashlib.md5()
|
||||||
basemd5.update(base_rom_bytes)
|
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."
|
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.")
|
"Get the correct game and version, then dump it.")
|
||||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
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():
|
def launch():
|
||||||
import colorama
|
import colorama
|
||||||
global executable, server_settings, server_args
|
global executable, server_settings, server_args
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
if server_settings:
|
if server_settings:
|
||||||
server_settings = os.path.abspath(server_settings)
|
server_settings = os.path.abspath(server_settings)
|
||||||
|
|||||||
@@ -8,8 +8,14 @@
|
|||||||
|
|
||||||
## Installing the Archipelago Mod using Lumafly
|
## Installing the Archipelago Mod using Lumafly
|
||||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||||
2. Click the "Install" button near the "Archipelago" mod entry.
|
2. Install the Archipelago mods by doing either of the following:
|
||||||
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
|
* 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!
|
3. Launch the game, you're all set!
|
||||||
|
|
||||||
### What to do if Lumafly fails to find your installation directory
|
### 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.")
|
parser = get_base_parser(description="KH1 Client, for text interfacing.")
|
||||||
|
|
||||||
args, rest = parser.parse_known_args()
|
args, rest = parser.parse_known_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -981,6 +981,6 @@ def launch():
|
|||||||
parser = get_base_parser(description="KH2 Client, for text interfacing.")
|
parser = get_base_parser(description="KH2 Client, for text interfacing.")
|
||||||
|
|
||||||
args, rest = parser.parse_known_args()
|
args, rest = parser.parse_known_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ SYNONYMS = {
|
|||||||
'JUMP': 'FEATHER',
|
'JUMP': 'FEATHER',
|
||||||
'PLUME': 'FEATHER',
|
'PLUME': 'FEATHER',
|
||||||
'WING': 'FEATHER',
|
'WING': 'FEATHER',
|
||||||
|
"QUILL": "FEATHER",
|
||||||
|
|
||||||
# SHOVEL
|
# SHOVEL
|
||||||
'DIG': 'SHOVEL',
|
'DIG': 'SHOVEL',
|
||||||
@@ -343,6 +344,8 @@ SYNONYMS = {
|
|||||||
# TRADING_ITEM_LETTER
|
# TRADING_ITEM_LETTER
|
||||||
'CARD': 'TRADING_ITEM_LETTER',
|
'CARD': 'TRADING_ITEM_LETTER',
|
||||||
'MESSAGE': 'TRADING_ITEM_LETTER',
|
'MESSAGE': 'TRADING_ITEM_LETTER',
|
||||||
|
"TICKET": 'TRADING_ITEM_LETTER',
|
||||||
|
"PASS": 'TRADING_ITEM_LETTER',
|
||||||
|
|
||||||
# TRADING_ITEM_BROOM
|
# TRADING_ITEM_BROOM
|
||||||
'SWEEP': 'TRADING_ITEM_BROOM',
|
'SWEEP': 'TRADING_ITEM_BROOM',
|
||||||
@@ -365,6 +368,8 @@ SYNONYMS = {
|
|||||||
'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||||
'SCOPE': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
'SCOPE': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||||
'XRAY': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
'XRAY': 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||||
|
"DETECTOR": 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||||
|
"ITEMFINDER": 'TRADING_ITEM_MAGNIFYING_GLASS',
|
||||||
|
|
||||||
# PIECE_OF_POWER
|
# PIECE_OF_POWER
|
||||||
'TRIANGLE': 'PIECE_OF_POWER',
|
'TRIANGLE': 'PIECE_OF_POWER',
|
||||||
@@ -378,6 +383,7 @@ PHRASES = {
|
|||||||
'BOSS KEY': 'NIGHTMARE_KEY',
|
'BOSS KEY': 'NIGHTMARE_KEY',
|
||||||
'HEART PIECE': 'HEART_PIECE',
|
'HEART PIECE': 'HEART_PIECE',
|
||||||
'PIECE OF HEART': 'HEART_PIECE',
|
'PIECE OF HEART': 'HEART_PIECE',
|
||||||
|
"ROCK SMASH": 'BOMB',
|
||||||
}
|
}
|
||||||
|
|
||||||
# All following will only be used to match items for the specific game.
|
# All following will only be used to match items for the specific game.
|
||||||
@@ -404,6 +410,16 @@ GAME_SPECIFIC_PHRASES = {
|
|||||||
|
|
||||||
'Ocarina of Time': {
|
'Ocarina of Time': {
|
||||||
'COJIRO': 'ROOSTER',
|
'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': {
|
'SMZ3': {
|
||||||
@@ -417,10 +433,14 @@ GAME_SPECIFIC_PHRASES = {
|
|||||||
|
|
||||||
'Sonic Adventure 2 Battle': {
|
'Sonic Adventure 2 Battle': {
|
||||||
'CHAOS EMERALD': 'PIECE_OF_POWER',
|
'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': {
|
'Super Mario 64': {
|
||||||
'POWER STAR': 'PIECE_OF_POWER',
|
'POWER STAR': 'PIECE_OF_POWER',
|
||||||
|
"Key": "NIGHTMARE_KEY" # Affect 2nd Floor / Basement / Progressive keys
|
||||||
},
|
},
|
||||||
|
|
||||||
'Super Mario World': {
|
'Super Mario World': {
|
||||||
@@ -528,4 +548,336 @@ GAME_SPECIFIC_PHRASES = {
|
|||||||
'2500 Tokens': 'RUPEES_500',
|
'2500 Tokens': 'RUPEES_500',
|
||||||
'5000 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])
|
phrases.update(ItemIconGuessing.GAME_SPECIFIC_PHRASES[foreign_game])
|
||||||
|
|
||||||
for phrase, icon in phrases.items():
|
for phrase, icon in phrases.items():
|
||||||
if phrase in uppered:
|
if phrase.upper() in uppered:
|
||||||
return icon
|
return icon
|
||||||
# pattern for breaking down camelCase, also separates out digits
|
# 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)")
|
pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=\d)")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class PortalPlando(PlandoConnections):
|
|||||||
- entrance: Searing Crags
|
- entrance: Searing Crags
|
||||||
exit: Glacial Peak Portal
|
exit: Glacial Peak Portal
|
||||||
"""
|
"""
|
||||||
|
display_name = "Portal Plando Connections"
|
||||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||||
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
||||||
checkpoints = [point for points in CHECKPOINTS.values() for point in points]
|
checkpoints = [point for points in CHECKPOINTS.values() for point in points]
|
||||||
@@ -48,6 +49,7 @@ class TransitionPlando(PlandoConnections):
|
|||||||
exit: Dark Cave - Right
|
exit: Dark Cave - Right
|
||||||
direction: both
|
direction: both
|
||||||
"""
|
"""
|
||||||
|
display_name = "Transition Plando Connections"
|
||||||
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
||||||
exits = frozenset(RANDOMIZED_CONNECTIONS.values())
|
exits = frozenset(RANDOMIZED_CONNECTIONS.values())
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class MessengerRules:
|
|||||||
self.connection_rules = {
|
self.connection_rules = {
|
||||||
# from ToTHQ
|
# from ToTHQ
|
||||||
"Artificer's Portal":
|
"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":
|
"Shrink Down":
|
||||||
lambda state: state.has_all(NOTES, self.player),
|
lambda state: state.has_all(NOTES, self.player),
|
||||||
# the shop
|
# the shop
|
||||||
@@ -267,6 +267,8 @@ class MessengerRules:
|
|||||||
# tower of time
|
# tower of time
|
||||||
"Tower of Time Seal - Time Waster":
|
"Tower of Time Seal - Time Waster":
|
||||||
self.has_dart,
|
self.has_dart,
|
||||||
|
# corrupted future
|
||||||
|
"Corrupted Future - Key of Courage": lambda state: state.has("Magic Firefly", self.player),
|
||||||
# cloud ruins
|
# cloud ruins
|
||||||
"Time Warp Mega Shard":
|
"Time Warp Mega Shard":
|
||||||
lambda state: self.has_vertical(state) or self.can_dboost(state),
|
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)
|
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)
|
multiworld.completion_condition[self.player] = lambda state: state.has("Do the Thing!", self.player)
|
||||||
if self.world.options.accessibility: # not locations accessibility
|
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):
|
class MessengerHardRules(MessengerRules):
|
||||||
@@ -530,9 +532,11 @@ class MessengerOOBRules(MessengerRules):
|
|||||||
self.world.options.accessibility.value = MessengerAccessibility.option_minimal
|
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
|
# 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("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("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")
|
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")
|
||||||
|
|||||||
@@ -100,14 +100,15 @@ class OOTWeb(WebWorld):
|
|||||||
["Edos"]
|
["Edos"]
|
||||||
)
|
)
|
||||||
|
|
||||||
setup_es = Tutorial(
|
# Very out of date, requires updating to match current
|
||||||
setup.tutorial_name,
|
# setup_es = Tutorial(
|
||||||
setup.description,
|
# setup.tutorial_name,
|
||||||
"Español",
|
# setup.description,
|
||||||
"setup_es.md",
|
# "Español",
|
||||||
"setup/es",
|
# "setup_es.md",
|
||||||
setup.authors
|
# "setup/es",
|
||||||
)
|
# setup.authors
|
||||||
|
# )
|
||||||
|
|
||||||
setup_fr = Tutorial(
|
setup_fr = Tutorial(
|
||||||
setup.tutorial_name,
|
setup.tutorial_name,
|
||||||
@@ -127,7 +128,7 @@ class OOTWeb(WebWorld):
|
|||||||
["Held_der_Zeit"]
|
["Held_der_Zeit"]
|
||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup, setup_es, setup_fr, setup_de]
|
tutorials = [setup, setup_fr, setup_de]
|
||||||
option_groups = oot_option_groups
|
option_groups = oot_option_groups
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
- New option `free_fly_blacklist` limits which cities can show up as a free fly location.
|
- 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.
|
- 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.
|
- 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
|
### Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class PokemonRedBlueWorld(World):
|
|||||||
self.dexsanity_table = []
|
self.dexsanity_table = []
|
||||||
self.trainersanity_table = []
|
self.trainersanity_table = []
|
||||||
self.local_locs = []
|
self.local_locs = []
|
||||||
|
self.pc_item = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||||
@@ -289,7 +290,9 @@ class PokemonRedBlueWorld(World):
|
|||||||
multiworld.random.shuffle(itempool)
|
multiworld.random.shuffle(itempool)
|
||||||
unplaced_items = []
|
unplaced_items = []
|
||||||
for i, item in enumerate(itempool):
|
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:
|
if item.advancement:
|
||||||
pool = progitempool
|
pool = progitempool
|
||||||
elif item.useful:
|
elif item.useful:
|
||||||
@@ -308,8 +311,6 @@ class PokemonRedBlueWorld(World):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
unplaced_items.append(item)
|
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]
|
progitempool += [item for item in unplaced_items if item.advancement]
|
||||||
usefulitempool += [item for item in unplaced_items if item.useful]
|
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)]
|
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:
|
if loc.item is None:
|
||||||
locs.add(loc)
|
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):
|
for loc in sorted(locs):
|
||||||
if loc.name in self.options.priority_locations.value:
|
if loc.name in self.options.priority_locations.value:
|
||||||
add_item_rule(loc, lambda i: i.advancement)
|
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":
|
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")
|
add_item_rule(loc, lambda i: i.name != "Oak's Parcel")
|
||||||
|
|
||||||
@@ -520,6 +518,14 @@ class PokemonRedBlueWorld(World):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
|
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
|
@classmethod
|
||||||
def stage_post_fill(cls, multiworld):
|
def stage_post_fill(cls, multiworld):
|
||||||
# Convert all but one of each instance of a wild Pokemon to useful classification.
|
# 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.item_pool.append(item)
|
||||||
|
|
||||||
world.random.shuffle(world.item_pool)
|
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] \
|
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.name for item in world.multiworld.precollected_items[world.player] if
|
||||||
item.advancement]
|
item.advancement]
|
||||||
|
|||||||
@@ -150,6 +150,26 @@ sample_chao_names = [
|
|||||||
"Hubert",
|
"Hubert",
|
||||||
"Corvus",
|
"Corvus",
|
||||||
"Nigel",
|
"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 = [
|
totally_real_item_names = [
|
||||||
@@ -240,6 +260,35 @@ totally_real_item_names = [
|
|||||||
"Ladder",
|
"Ladder",
|
||||||
|
|
||||||
"Visible Dots",
|
"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 = [
|
all_exits = [
|
||||||
|
|||||||
@@ -1,6 +1,82 @@
|
|||||||
# Sonic Adventure 2 Battle - Changelog
|
# 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
|
## v2.3 - The Chao Update
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.AutoWorld import World
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
speed_characters_1 = "Sonic vs Shadow 1"
|
from .Names import LocationName
|
||||||
speed_characters_2 = "Sonic vs Shadow 2"
|
from .Options import GateBossPlando
|
||||||
mech_characters_1 = "Tails vs Eggman 1"
|
|
||||||
mech_characters_2 = "Tails vs Eggman 2"
|
|
||||||
hunt_characters_1 = "Knuckles vs Rouge 1"
|
speed_characters_1 = "sonic vs shadow 1"
|
||||||
big_foot = "F-6t BIG FOOT"
|
speed_characters_2 = "sonic vs shadow 2"
|
||||||
hot_shot = "B-3x HOT SHOT"
|
mech_characters_1 = "tails vs eggman 1"
|
||||||
flying_dog = "R-1/A FLYING DOG"
|
mech_characters_2 = "tails vs eggman 2"
|
||||||
egg_golem_sonic = "Egg Golem (Sonic)"
|
hunt_characters_1 = "knuckles vs rouge 1"
|
||||||
egg_golem_eggman = "Egg Golem (Eggman)"
|
big_foot = "big foot"
|
||||||
king_boom_boo = "King Boom Boo"
|
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 = {
|
gate_bosses_no_requirements_table = {
|
||||||
speed_characters_1: 0,
|
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):
|
def get_boss_name(boss: int):
|
||||||
for key, value in gate_bosses_no_requirements_table.items():
|
return boss_id_to_name[boss]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def boss_has_requirement(boss: int):
|
def boss_has_requirement(boss: int):
|
||||||
return boss >= len(gate_bosses_no_requirements_table)
|
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] = []
|
selected_bosses: typing.List[int] = []
|
||||||
boss_gates: typing.List[int] = []
|
boss_gates: typing.List[int] = []
|
||||||
available_bosses: typing.List[str] = list(gate_bosses_no_requirements_table.keys())
|
available_bosses: typing.List[str] = list(gate_bosses_no_requirements_table.keys())
|
||||||
multiworld.random.shuffle(available_bosses)
|
world.random.shuffle(available_bosses)
|
||||||
halfway = False
|
|
||||||
|
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):
|
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)
|
available_bosses.extend(gate_bosses_with_requirements_table)
|
||||||
multiworld.random.shuffle(available_bosses)
|
world.random.shuffle(available_bosses)
|
||||||
halfway = True
|
|
||||||
selected_bosses.append(all_gate_bosses_table[available_bosses[0]])
|
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)
|
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))
|
bosses: typing.Dict[int, int] = dict(zip(boss_gates, selected_bosses))
|
||||||
|
|
||||||
return 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:
|
if world.options.boss_rush_shuffle == 0:
|
||||||
boss_list_o = list(range(0, 16))
|
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:
|
elif world.options.boss_rush_shuffle == 1:
|
||||||
boss_list_o = list(range(0, 16))
|
boss_list_o = list(range(0, 16))
|
||||||
boss_list_s = boss_list_o.copy()
|
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))
|
return dict(zip(boss_list_o, boss_list_s))
|
||||||
elif world.options.boss_rush_shuffle == 2:
|
elif world.options.boss_rush_shuffle == 2:
|
||||||
boss_list_o = list(range(0, 16))
|
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:
|
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))
|
return dict(zip(boss_list_o, boss_list_s))
|
||||||
elif world.options.boss_rush_shuffle == 3:
|
elif world.options.boss_rush_shuffle == 3:
|
||||||
boss_list_o = list(range(0, 16))
|
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:
|
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))
|
return dict(zip(boss_list_o, boss_list_s))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import typing
|
|||||||
|
|
||||||
from BaseClasses import Item, ItemClassification
|
from BaseClasses import Item, ItemClassification
|
||||||
from .Names import ItemName
|
from .Names import ItemName
|
||||||
from worlds.alttp import ALTTPWorld
|
|
||||||
|
|
||||||
|
|
||||||
class ItemData(typing.NamedTuple):
|
class ItemData(typing.NamedTuple):
|
||||||
@@ -14,7 +13,7 @@ class ItemData(typing.NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class SA2BItem(Item):
|
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):
|
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
|
||||||
super(SA2BItem, self).__init__(name, classification, code, player)
|
super(SA2BItem, self).__init__(name, classification, code, player)
|
||||||
@@ -73,19 +72,36 @@ junk_table = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trap_table = {
|
trap_table = {
|
||||||
ItemName.omochao_trap: ItemData(0xFF0030, False, True),
|
ItemName.omochao_trap: ItemData(0xFF0030, False, True),
|
||||||
ItemName.timestop_trap: ItemData(0xFF0031, False, True),
|
ItemName.timestop_trap: ItemData(0xFF0031, False, True),
|
||||||
ItemName.confuse_trap: ItemData(0xFF0032, False, True),
|
ItemName.confuse_trap: ItemData(0xFF0032, False, True),
|
||||||
ItemName.tiny_trap: ItemData(0xFF0033, False, True),
|
ItemName.tiny_trap: ItemData(0xFF0033, False, True),
|
||||||
ItemName.gravity_trap: ItemData(0xFF0034, False, True),
|
ItemName.gravity_trap: ItemData(0xFF0034, False, True),
|
||||||
ItemName.exposition_trap: ItemData(0xFF0035, False, True),
|
ItemName.exposition_trap: ItemData(0xFF0035, False, True),
|
||||||
#ItemName.darkness_trap: ItemData(0xFF0036, False, True),
|
#ItemName.darkness_trap: ItemData(0xFF0036, False, True),
|
||||||
ItemName.ice_trap: ItemData(0xFF0037, False, True),
|
ItemName.ice_trap: ItemData(0xFF0037, False, True),
|
||||||
ItemName.slow_trap: ItemData(0xFF0038, False, True),
|
ItemName.slow_trap: ItemData(0xFF0038, False, True),
|
||||||
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
|
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
|
||||||
ItemName.reverse_trap: ItemData(0xFF003A, 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 = {
|
emeralds_table = {
|
||||||
@@ -235,7 +251,7 @@ chaos_drives_table = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event_table = {
|
event_table = {
|
||||||
ItemName.maria: ItemData(0xFF001D, True),
|
ItemName.maria: ItemData(None, True),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Complete item table.
|
# Complete item table.
|
||||||
@@ -244,6 +260,7 @@ item_table = {
|
|||||||
**upgrades_table,
|
**upgrades_table,
|
||||||
**junk_table,
|
**junk_table,
|
||||||
**trap_table,
|
**trap_table,
|
||||||
|
**minigame_trap_table,
|
||||||
**emeralds_table,
|
**emeralds_table,
|
||||||
**eggs_table,
|
**eggs_table,
|
||||||
**fruits_table,
|
**fruits_table,
|
||||||
@@ -251,7 +268,6 @@ item_table = {
|
|||||||
**hats_table,
|
**hats_table,
|
||||||
**animals_table,
|
**animals_table,
|
||||||
**chaos_drives_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}
|
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()),
|
"Seeds": list(seeds_table.keys()),
|
||||||
"Hats": list(hats_table.keys()),
|
"Hats": list(hats_table.keys()),
|
||||||
"Traps": list(trap_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"
|
try:
|
||||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
|
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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -119,11 +119,14 @@ mission_orders: typing.List[typing.List[int]] = [
|
|||||||
[4, 5, 3, 2, 1],
|
[4, 5, 3, 2, 1],
|
||||||
]
|
]
|
||||||
|
|
||||||
### 0: Speed
|
### 0: Sonic
|
||||||
### 1: Mech
|
### 1: Tails
|
||||||
### 2: Hunt
|
### 2: Knuckles
|
||||||
### 3: Kart
|
### 3: Shadow
|
||||||
### 4: Cannon's Core
|
### 4: Eggman
|
||||||
|
### 5: Rouge
|
||||||
|
### 6: Kart
|
||||||
|
### 7: Cannon's Core
|
||||||
level_styles: typing.List[int] = [
|
level_styles: typing.List[int] = [
|
||||||
0,
|
0,
|
||||||
2,
|
2,
|
||||||
@@ -133,7 +136,7 @@ level_styles: typing.List[int] = [
|
|||||||
2,
|
2,
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
3,
|
6,
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
2,
|
2,
|
||||||
@@ -142,22 +145,22 @@ level_styles: typing.List[int] = [
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
|
|
||||||
4,
|
4,
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
|
||||||
|
7,
|
||||||
]
|
]
|
||||||
|
|
||||||
stage_name_prefixes: typing.List[str] = [
|
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):
|
for level in range(31):
|
||||||
mission_count_table[level] = 0
|
mission_count_table[level] = 0
|
||||||
else:
|
else:
|
||||||
speed_active_missions = 1
|
sonic_active_missions = 1
|
||||||
mech_active_missions = 1
|
tails_active_missions = 1
|
||||||
hunt_active_missions = 1
|
knuckles_active_missions = 1
|
||||||
|
shadow_active_missions = 1
|
||||||
|
eggman_active_missions = 1
|
||||||
|
rouge_active_missions = 1
|
||||||
kart_active_missions = 1
|
kart_active_missions = 1
|
||||||
cannons_core_active_missions = 1
|
cannons_core_active_missions = 1
|
||||||
|
|
||||||
for i in range(2,6):
|
for i in range(2,6):
|
||||||
if getattr(world.options, "speed_mission_" + str(i), None):
|
if getattr(world.options, "sonic_mission_" + str(i), None):
|
||||||
speed_active_missions += 1
|
sonic_active_missions += 1
|
||||||
|
|
||||||
if getattr(world.options, "mech_mission_" + str(i), None):
|
if getattr(world.options, "tails_mission_" + str(i), None):
|
||||||
mech_active_missions += 1
|
tails_active_missions += 1
|
||||||
|
|
||||||
if getattr(world.options, "hunt_mission_" + str(i), None):
|
if getattr(world.options, "knuckles_mission_" + str(i), None):
|
||||||
hunt_active_missions += 1
|
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):
|
if getattr(world.options, "kart_mission_" + str(i), None):
|
||||||
kart_active_missions += 1
|
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):
|
if getattr(world.options, "cannons_core_mission_" + str(i), None):
|
||||||
cannons_core_active_missions += 1
|
cannons_core_active_missions += 1
|
||||||
|
|
||||||
speed_active_missions = min(speed_active_missions, world.options.speed_mission_count.value)
|
sonic_active_missions = min(sonic_active_missions, world.options.sonic_mission_count.value)
|
||||||
mech_active_missions = min(mech_active_missions, world.options.mech_mission_count.value)
|
tails_active_missions = min(tails_active_missions, world.options.tails_mission_count.value)
|
||||||
hunt_active_missions = min(hunt_active_missions, world.options.hunt_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)
|
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)
|
cannons_core_active_missions = min(cannons_core_active_missions, world.options.cannons_core_mission_count.value)
|
||||||
|
|
||||||
active_missions: typing.List[typing.List[int]] = [
|
active_missions: typing.List[typing.List[int]] = [
|
||||||
speed_active_missions,
|
sonic_active_missions,
|
||||||
mech_active_missions,
|
tails_active_missions,
|
||||||
hunt_active_missions,
|
knuckles_active_missions,
|
||||||
|
shadow_active_missions,
|
||||||
|
eggman_active_missions,
|
||||||
|
rouge_active_missions,
|
||||||
kart_active_missions,
|
kart_active_missions,
|
||||||
cannons_core_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):
|
for level in range(31):
|
||||||
mission_table[level] = 0
|
mission_table[level] = 0
|
||||||
else:
|
else:
|
||||||
speed_active_missions: typing.List[int] = [1]
|
sonic_active_missions: typing.List[int] = [1]
|
||||||
mech_active_missions: typing.List[int] = [1]
|
tails_active_missions: typing.List[int] = [1]
|
||||||
hunt_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]
|
kart_active_missions: typing.List[int] = [1]
|
||||||
cannons_core_active_missions: typing.List[int] = [1]
|
cannons_core_active_missions: typing.List[int] = [1]
|
||||||
|
|
||||||
# Add included missions
|
# Add included missions
|
||||||
for i in range(2,6):
|
for i in range(2,6):
|
||||||
if getattr(world.options, "speed_mission_" + str(i), None):
|
if getattr(world.options, "sonic_mission_" + str(i), None):
|
||||||
speed_active_missions.append(i)
|
sonic_active_missions.append(i)
|
||||||
|
|
||||||
if getattr(world.options, "mech_mission_" + str(i), None):
|
if getattr(world.options, "tails_mission_" + str(i), None):
|
||||||
mech_active_missions.append(i)
|
tails_active_missions.append(i)
|
||||||
|
|
||||||
if getattr(world.options, "hunt_mission_" + str(i), None):
|
if getattr(world.options, "knuckles_mission_" + str(i), None):
|
||||||
hunt_active_missions.append(i)
|
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):
|
if getattr(world.options, "kart_mission_" + str(i), None):
|
||||||
kart_active_missions.append(i)
|
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)
|
cannons_core_active_missions.append(i)
|
||||||
|
|
||||||
active_missions: typing.List[typing.List[int]] = [
|
active_missions: typing.List[typing.List[int]] = [
|
||||||
speed_active_missions,
|
sonic_active_missions,
|
||||||
mech_active_missions,
|
tails_active_missions,
|
||||||
hunt_active_missions,
|
knuckles_active_missions,
|
||||||
|
shadow_active_missions,
|
||||||
|
eggman_active_missions,
|
||||||
|
rouge_active_missions,
|
||||||
kart_active_missions,
|
kart_active_missions,
|
||||||
cannons_core_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]):
|
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_count = mission_count_map[30]
|
||||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||||
stage_prefix: str = stage_name_prefixes[30]
|
stage_prefix: str = stage_name_prefixes[30]
|
||||||
|
|
||||||
first_mission_number = mission_order[0]
|
first_mission_number = mission_order[0]
|
||||||
last_mission_number = mission_order[mission_count - 1]
|
last_mission_number = mission_order[mission_count - 1]
|
||||||
first_location_name: str = stage_prefix + str(first_mission_number)
|
first_location_name: str = stage_prefix + str(first_mission_number)
|
||||||
last_location_name: str = stage_prefix + str(last_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"
|
market_token = "Chao Coin"
|
||||||
|
|
||||||
# Upgrade Definitions
|
# Upgrade Definitions
|
||||||
sonic_gloves = "Sonic - Magic Glove"
|
sonic_gloves = "Sonic - Magic Gloves"
|
||||||
sonic_light_shoes = "Sonic - Light Shoes"
|
sonic_light_shoes = "Sonic - Light Shoes"
|
||||||
sonic_ancient_light = "Sonic - Ancient Light"
|
sonic_ancient_light = "Sonic - Ancient Light"
|
||||||
sonic_bounce_bracelet = "Sonic - Bounce Bracelet"
|
sonic_bounce_bracelet = "Sonic - Bounce Bracelet"
|
||||||
@@ -51,19 +51,34 @@ invincibility = "Invincibility"
|
|||||||
|
|
||||||
|
|
||||||
# Traps
|
# Traps
|
||||||
omochao_trap = "OmoTrap"
|
omochao_trap = "OmoTrap"
|
||||||
timestop_trap = "Chaos Control Trap"
|
timestop_trap = "Chaos Control Trap"
|
||||||
confuse_trap = "Confusion Trap"
|
confuse_trap = "Confusion Trap"
|
||||||
tiny_trap = "Tiny Trap"
|
tiny_trap = "Tiny Trap"
|
||||||
gravity_trap = "Gravity Trap"
|
gravity_trap = "Gravity Trap"
|
||||||
exposition_trap = "Exposition Trap"
|
exposition_trap = "Exposition Trap"
|
||||||
darkness_trap = "Darkness Trap"
|
darkness_trap = "Darkness Trap"
|
||||||
ice_trap = "Ice Trap"
|
ice_trap = "Ice Trap"
|
||||||
slow_trap = "Slow Trap"
|
slow_trap = "Slow Trap"
|
||||||
cutscene_trap = "Cutscene Trap"
|
cutscene_trap = "Cutscene Trap"
|
||||||
reverse_trap = "Reverse 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
|
# Chaos Emeralds
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
from dataclasses import dataclass
|
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):
|
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
|
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
|
Chaos Chao: Raise a Chaos Chao to win
|
||||||
|
|
||||||
|
Minigame Madness: Win a certain amount of each Minigame Trap, then defeat Finalhazard
|
||||||
"""
|
"""
|
||||||
display_name = "Goal"
|
display_name = "Goal"
|
||||||
option_biolizard = 0
|
option_biolizard = 0
|
||||||
@@ -32,6 +36,7 @@ class Goal(Choice):
|
|||||||
option_cannons_core_boss_rush = 5
|
option_cannons_core_boss_rush = 5
|
||||||
option_boss_rush_chaos_emerald_hunt = 6
|
option_boss_rush_chaos_emerald_hunt = 6
|
||||||
option_chaos_chao = 7
|
option_chaos_chao = 7
|
||||||
|
option_minigame_madness = 8
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -71,6 +76,66 @@ class BossRushShuffle(Choice):
|
|||||||
default = 0
|
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):
|
class BaseTrapWeight(Choice):
|
||||||
"""
|
"""
|
||||||
Base Class for Trap Weights
|
Base Class for Trap Weights
|
||||||
@@ -159,6 +224,34 @@ class ReverseTrapWeight(BaseTrapWeight):
|
|||||||
display_name = "Reverse Trap Weight"
|
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):
|
class PongTrapWeight(BaseTrapWeight):
|
||||||
"""
|
"""
|
||||||
Likelihood of receiving a trap which forces you to play a Pong minigame
|
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"
|
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):
|
class MinigameTrapDifficulty(Choice):
|
||||||
"""
|
"""
|
||||||
How difficult any Minigame-style traps are
|
How difficult any Minigame-style traps are
|
||||||
|
Chaos causes the difficulty to be random per-minigame
|
||||||
"""
|
"""
|
||||||
display_name = "Minigame Trap Difficulty"
|
display_name = "Minigame Trap Difficulty"
|
||||||
option_easy = 0
|
option_easy = 0
|
||||||
option_medium = 1
|
option_medium = 1
|
||||||
option_hard = 2
|
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
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
@@ -197,7 +382,7 @@ class TrapFillPercentage(Range):
|
|||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class Keysanity(Toggle):
|
class Keysanity(DefaultOnToggle):
|
||||||
"""
|
"""
|
||||||
Determines whether picking up Chao Keys grants checks
|
Determines whether picking up Chao Keys grants checks
|
||||||
(86 Locations)
|
(86 Locations)
|
||||||
@@ -225,7 +410,7 @@ class Whistlesanity(Choice):
|
|||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class Beetlesanity(Toggle):
|
class Beetlesanity(DefaultOnToggle):
|
||||||
"""
|
"""
|
||||||
Determines whether destroying Gold Beetles grants checks
|
Determines whether destroying Gold Beetles grants checks
|
||||||
(27 Locations)
|
(27 Locations)
|
||||||
@@ -244,13 +429,35 @@ class Omosanity(Toggle):
|
|||||||
class Animalsanity(Toggle):
|
class Animalsanity(Toggle):
|
||||||
"""
|
"""
|
||||||
Determines whether unique counts of animals grant checks.
|
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.
|
ALL animals must be collected in a single run of a mission to get all checks.
|
||||||
"""
|
"""
|
||||||
display_name = "Animalsanity"
|
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):
|
class KartRaceChecks(Choice):
|
||||||
"""
|
"""
|
||||||
Determines whether Kart Race Mode grants checks
|
Determines whether Kart Race Mode grants checks
|
||||||
@@ -313,7 +520,7 @@ class LevelGateCosts(Choice):
|
|||||||
option_low = 0
|
option_low = 0
|
||||||
option_medium = 1
|
option_medium = 1
|
||||||
option_high = 2
|
option_high = 2
|
||||||
default = 2
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class MaximumEmblemCap(Range):
|
class MaximumEmblemCap(Range):
|
||||||
@@ -523,109 +730,214 @@ class BaseMissionCount(Range):
|
|||||||
default = 2
|
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):
|
class KartMissionCount(BaseMissionCount):
|
||||||
@@ -706,7 +1018,7 @@ class RingLoss(Choice):
|
|||||||
|
|
||||||
Modern: You lose 20 rings when hit
|
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"
|
display_name = "Ring Loss"
|
||||||
option_classic = 0
|
option_classic = 0
|
||||||
@@ -729,6 +1041,16 @@ class RingLink(Toggle):
|
|||||||
display_name = "Ring Link"
|
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):
|
class SADXMusic(Choice):
|
||||||
"""
|
"""
|
||||||
Whether the randomizer will include Sonic Adventure DX Music in the music pool
|
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
|
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"
|
display_name = "Logic Difficulty"
|
||||||
option_standard = 0
|
option_standard = 0
|
||||||
option_hard = 1
|
option_hard = 1
|
||||||
|
option_expert = 2
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -835,6 +1160,8 @@ sa2b_option_groups = [
|
|||||||
OptionGroup("General Options", [
|
OptionGroup("General Options", [
|
||||||
Goal,
|
Goal,
|
||||||
BossRushShuffle,
|
BossRushShuffle,
|
||||||
|
MinigameMadnessRequirement,
|
||||||
|
MinigameMadnessMinimum,
|
||||||
LogicDifficulty,
|
LogicDifficulty,
|
||||||
RequiredRank,
|
RequiredRank,
|
||||||
MaximumEmblemCap,
|
MaximumEmblemCap,
|
||||||
@@ -854,6 +1181,8 @@ sa2b_option_groups = [
|
|||||||
Beetlesanity,
|
Beetlesanity,
|
||||||
Omosanity,
|
Omosanity,
|
||||||
Animalsanity,
|
Animalsanity,
|
||||||
|
ItemBoxsanity,
|
||||||
|
Bigsanity,
|
||||||
KartRaceChecks,
|
KartRaceChecks,
|
||||||
]),
|
]),
|
||||||
OptionGroup("Chao", [
|
OptionGroup("Chao", [
|
||||||
@@ -885,29 +1214,68 @@ sa2b_option_groups = [
|
|||||||
SlowTrapWeight,
|
SlowTrapWeight,
|
||||||
CutsceneTrapWeight,
|
CutsceneTrapWeight,
|
||||||
ReverseTrapWeight,
|
ReverseTrapWeight,
|
||||||
|
LiteratureTrapWeight,
|
||||||
|
ControllerDriftTrapWeight,
|
||||||
|
PoisonTrapWeight,
|
||||||
|
BeeTrapWeight,
|
||||||
|
]),
|
||||||
|
OptionGroup("Minigames", [
|
||||||
PongTrapWeight,
|
PongTrapWeight,
|
||||||
|
BreakoutTrapWeight,
|
||||||
|
FishingTrapWeight,
|
||||||
|
TriviaTrapWeight,
|
||||||
|
PokemonTriviaTrapWeight,
|
||||||
|
PokemonCountTrapWeight,
|
||||||
|
NumberSequenceTrapWeight,
|
||||||
|
LightUpPathTrapWeight,
|
||||||
|
PinballTrapWeight,
|
||||||
|
MathQuizTrapWeight,
|
||||||
|
SnakeTrapWeight,
|
||||||
|
InputSequenceTrapWeight,
|
||||||
MinigameTrapDifficulty,
|
MinigameTrapDifficulty,
|
||||||
|
BigFishingDifficulty,
|
||||||
]),
|
]),
|
||||||
OptionGroup("Speed Missions", [
|
OptionGroup("Sonic Missions", [
|
||||||
SpeedMissionCount,
|
SonicMissionCount,
|
||||||
SpeedMission2,
|
SonicMission2,
|
||||||
SpeedMission3,
|
SonicMission3,
|
||||||
SpeedMission4,
|
SonicMission4,
|
||||||
SpeedMission5,
|
SonicMission5,
|
||||||
]),
|
]),
|
||||||
OptionGroup("Mech Missions", [
|
OptionGroup("Shadow Missions", [
|
||||||
MechMissionCount,
|
ShadowMissionCount,
|
||||||
MechMission2,
|
ShadowMission2,
|
||||||
MechMission3,
|
ShadowMission3,
|
||||||
MechMission4,
|
ShadowMission4,
|
||||||
MechMission5,
|
ShadowMission5,
|
||||||
]),
|
]),
|
||||||
OptionGroup("Hunt Missions", [
|
OptionGroup("Tails Missions", [
|
||||||
HuntMissionCount,
|
TailsMissionCount,
|
||||||
HuntMission2,
|
TailsMission2,
|
||||||
HuntMission3,
|
TailsMission3,
|
||||||
HuntMission4,
|
TailsMission4,
|
||||||
HuntMission5,
|
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", [
|
OptionGroup("Kart Missions", [
|
||||||
KartMissionCount,
|
KartMissionCount,
|
||||||
@@ -931,11 +1299,13 @@ sa2b_option_groups = [
|
|||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SA2BOptions(PerGameCommonOptions):
|
class SA2BOptions(PerGameCommonOptions):
|
||||||
goal: Goal
|
goal: Goal
|
||||||
boss_rush_shuffle: BossRushShuffle
|
boss_rush_shuffle: BossRushShuffle
|
||||||
|
minigame_madness_requirement: MinigameMadnessRequirement
|
||||||
|
minigame_madness_minimum: MinigameMadnessMinimum
|
||||||
|
gate_boss_plando: GateBossPlando
|
||||||
logic_difficulty: LogicDifficulty
|
logic_difficulty: LogicDifficulty
|
||||||
required_rank: RequiredRank
|
required_rank: RequiredRank
|
||||||
max_emblem_cap: MaximumEmblemCap
|
max_emblem_cap: MaximumEmblemCap
|
||||||
@@ -953,6 +1323,8 @@ class SA2BOptions(PerGameCommonOptions):
|
|||||||
beetlesanity: Beetlesanity
|
beetlesanity: Beetlesanity
|
||||||
omosanity: Omosanity
|
omosanity: Omosanity
|
||||||
animalsanity: Animalsanity
|
animalsanity: Animalsanity
|
||||||
|
itemboxsanity: ItemBoxsanity
|
||||||
|
bigsanity: Bigsanity
|
||||||
kart_race_checks: KartRaceChecks
|
kart_race_checks: KartRaceChecks
|
||||||
|
|
||||||
black_market_slots: BlackMarketSlots
|
black_market_slots: BlackMarketSlots
|
||||||
@@ -983,31 +1355,65 @@ class SA2BOptions(PerGameCommonOptions):
|
|||||||
slow_trap_weight: SlowTrapWeight
|
slow_trap_weight: SlowTrapWeight
|
||||||
cutscene_trap_weight: CutsceneTrapWeight
|
cutscene_trap_weight: CutsceneTrapWeight
|
||||||
reverse_trap_weight: ReverseTrapWeight
|
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
|
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
|
minigame_trap_difficulty: MinigameTrapDifficulty
|
||||||
|
big_fishing_difficulty: BigFishingDifficulty
|
||||||
|
|
||||||
sadx_music: SADXMusic
|
sadx_music: SADXMusic
|
||||||
music_shuffle: MusicShuffle
|
music_shuffle: MusicShuffle
|
||||||
voice_shuffle: VoiceShuffle
|
voice_shuffle: VoiceShuffle
|
||||||
narrator: Narrator
|
narrator: Narrator
|
||||||
|
|
||||||
speed_mission_count: SpeedMissionCount
|
sonic_mission_count: SonicMissionCount
|
||||||
speed_mission_2: SpeedMission2
|
sonic_mission_2: SonicMission2
|
||||||
speed_mission_3: SpeedMission3
|
sonic_mission_3: SonicMission3
|
||||||
speed_mission_4: SpeedMission4
|
sonic_mission_4: SonicMission4
|
||||||
speed_mission_5: SpeedMission5
|
sonic_mission_5: SonicMission5
|
||||||
|
|
||||||
mech_mission_count: MechMissionCount
|
shadow_mission_count: ShadowMissionCount
|
||||||
mech_mission_2: MechMission2
|
shadow_mission_2: ShadowMission2
|
||||||
mech_mission_3: MechMission3
|
shadow_mission_3: ShadowMission3
|
||||||
mech_mission_4: MechMission4
|
shadow_mission_4: ShadowMission4
|
||||||
mech_mission_5: MechMission5
|
shadow_mission_5: ShadowMission5
|
||||||
|
|
||||||
hunt_mission_count: HuntMissionCount
|
tails_mission_count: TailsMissionCount
|
||||||
hunt_mission_2: HuntMission2
|
tails_mission_2: TailsMission2
|
||||||
hunt_mission_3: HuntMission3
|
tails_mission_3: TailsMission3
|
||||||
hunt_mission_4: HuntMission4
|
tails_mission_4: TailsMission4
|
||||||
hunt_mission_5: HuntMission5
|
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_count: KartMissionCount
|
||||||
kart_mission_2: KartMission2
|
kart_mission_2: KartMission2
|
||||||
@@ -1022,4 +1428,5 @@ class SA2BOptions(PerGameCommonOptions):
|
|||||||
cannons_core_mission_5: CannonsCoreMission5
|
cannons_core_mission_5: CannonsCoreMission5
|
||||||
|
|
||||||
ring_link: RingLink
|
ring_link: RingLink
|
||||||
|
trap_link: TrapLink
|
||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
|||||||
502
worlds/sa2b/Presets.py
Normal file
502
worlds/sa2b/Presets.py
Normal file
@@ -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,
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
2115
worlds/sa2b/Rules.py
2115
worlds/sa2b/Rules.py
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,13 @@ from worlds.AutoWorld import WebWorld, World
|
|||||||
from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \
|
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
|
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 .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, \
|
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
|
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table, event_table
|
||||||
from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_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
|
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 .Names import ItemName, LocationName
|
||||||
from .Options import SA2BOptions, sa2b_option_groups
|
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, \
|
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
|
||||||
gate_0_blacklist_regions
|
gate_0_blacklist_regions
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
@@ -33,6 +34,7 @@ class SA2BWeb(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup_en]
|
tutorials = [setup_en]
|
||||||
option_groups = sa2b_option_groups
|
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):
|
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
|
topology_present = False
|
||||||
|
|
||||||
item_name_groups = item_groups
|
item_name_groups = item_groups
|
||||||
|
location_name_groups = location_groups
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
location_name_to_id = all_locations
|
location_name_to_id = all_locations
|
||||||
|
|
||||||
location_table: typing.Dict[str, int]
|
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_map: typing.Dict[int, int]
|
||||||
mission_count_map: typing.Dict[int, int]
|
mission_count_map: typing.Dict[int, int]
|
||||||
emblems_for_cannons_core: int
|
emblems_for_cannons_core: int
|
||||||
@@ -78,7 +83,7 @@ class SA2BWorld(World):
|
|||||||
|
|
||||||
def fill_slot_data(self) -> dict:
|
def fill_slot_data(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"ModVersion": 203,
|
"ModVersion": 204,
|
||||||
"Goal": self.options.goal.value,
|
"Goal": self.options.goal.value,
|
||||||
"MusicMap": self.generate_music_data(),
|
"MusicMap": self.generate_music_data(),
|
||||||
"VoiceMap": self.generate_voice_data(),
|
"VoiceMap": self.generate_voice_data(),
|
||||||
@@ -89,14 +94,20 @@ class SA2BWorld(World):
|
|||||||
"MusicShuffle": self.options.music_shuffle.value,
|
"MusicShuffle": self.options.music_shuffle.value,
|
||||||
"Narrator": self.options.narrator.value,
|
"Narrator": self.options.narrator.value,
|
||||||
"MinigameTrapDifficulty": self.options.minigame_trap_difficulty.value,
|
"MinigameTrapDifficulty": self.options.minigame_trap_difficulty.value,
|
||||||
|
"BigFishingDifficulty": self.options.big_fishing_difficulty.value,
|
||||||
"RingLoss": self.options.ring_loss.value,
|
"RingLoss": self.options.ring_loss.value,
|
||||||
"RingLink": self.options.ring_link.value,
|
"RingLink": self.options.ring_link.value,
|
||||||
|
"TrapLink": self.options.trap_link.value,
|
||||||
"RequiredRank": self.options.required_rank.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,
|
"ChaoKeys": self.options.keysanity.value,
|
||||||
"Whistlesanity": self.options.whistlesanity.value,
|
"Whistlesanity": self.options.whistlesanity.value,
|
||||||
"GoldBeetles": self.options.beetlesanity.value,
|
"GoldBeetles": self.options.beetlesanity.value,
|
||||||
"OmochaoChecks": self.options.omosanity.value,
|
"OmochaoChecks": self.options.omosanity.value,
|
||||||
"AnimalChecks": self.options.animalsanity.value,
|
"AnimalChecks": self.options.animalsanity.value,
|
||||||
|
"ItemBoxChecks": self.options.itemboxsanity.value,
|
||||||
|
"BigChecks": self.options.bigsanity.value,
|
||||||
"KartRaceChecks": self.options.kart_race_checks.value,
|
"KartRaceChecks": self.options.kart_race_checks.value,
|
||||||
"ChaoStadiumChecks": self.options.chao_stadium_checks.value,
|
"ChaoStadiumChecks": self.options.chao_stadium_checks.value,
|
||||||
"ChaoRaceDifficulty": self.options.chao_race_difficulty.value,
|
"ChaoRaceDifficulty": self.options.chao_race_difficulty.value,
|
||||||
@@ -122,6 +133,7 @@ class SA2BWorld(World):
|
|||||||
"GateCosts": self.gate_costs,
|
"GateCosts": self.gate_costs,
|
||||||
"GateBosses": self.gate_bosses,
|
"GateBosses": self.gate_bosses,
|
||||||
"BossRushMap": self.boss_rush_map,
|
"BossRushMap": self.boss_rush_map,
|
||||||
|
"ActiveTraps": self.output_active_traps(),
|
||||||
"PlayerNum": self.player,
|
"PlayerNum": self.player,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,12 +163,42 @@ class SA2BWorld(World):
|
|||||||
|
|
||||||
valid_trap_weights = self.options.exposition_trap_weight.value + \
|
valid_trap_weights = self.options.exposition_trap_weight.value + \
|
||||||
self.options.reverse_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:
|
if valid_trap_weights == 0:
|
||||||
self.options.exposition_trap_weight.value = 4
|
self.options.exposition_trap_weight.value = 4
|
||||||
self.options.reverse_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.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:
|
if self.options.kart_race_checks.value == 0:
|
||||||
self.options.kart_race_checks.value = 2
|
self.options.kart_race_checks.value = 2
|
||||||
@@ -164,8 +206,8 @@ class SA2BWorld(World):
|
|||||||
self.gate_bosses = {}
|
self.gate_bosses = {}
|
||||||
self.boss_rush_map = {}
|
self.boss_rush_map = {}
|
||||||
else:
|
else:
|
||||||
self.gate_bosses = get_gate_bosses(self.multiworld, self)
|
self.gate_bosses = get_gate_bosses(self)
|
||||||
self.boss_rush_map = get_boss_rush_bosses(self.multiworld, self)
|
self.boss_rush_map = get_boss_rush_bosses(self)
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
self.mission_map = get_mission_table(self.multiworld, self, self.player)
|
self.mission_map = get_mission_table(self.multiworld, self, self.player)
|
||||||
@@ -177,7 +219,7 @@ class SA2BWorld(World):
|
|||||||
# Not Generate Basic
|
# Not Generate Basic
|
||||||
self.black_market_costs = dict()
|
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))
|
self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||||
elif self.options.goal.value == 1:
|
elif self.options.goal.value == 1:
|
||||||
self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria))
|
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:
|
if self.options.goal.value != 3:
|
||||||
# Fill item pool with all required items
|
# Fill item pool with all required items
|
||||||
for item in {**upgrades_table}:
|
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]:
|
if self.options.goal.value in [1, 2, 6]:
|
||||||
# Some flavor of Chaos Emerald Hunt
|
# Some flavor of Chaos Emerald Hunt
|
||||||
@@ -212,6 +254,25 @@ class SA2BWorld(World):
|
|||||||
# Black Market
|
# Black Market
|
||||||
itempool += [self.create_item(ItemName.market_token) for _ in range(self.options.black_market_slots.value)]
|
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
|
black_market_unlock_mult = 1.0
|
||||||
if self.options.black_market_unlock_costs.value == 0:
|
if self.options.black_market_unlock_costs.value == 0:
|
||||||
black_market_unlock_mult = 0.5
|
black_market_unlock_mult = 0.5
|
||||||
@@ -235,12 +296,12 @@ class SA2BWorld(World):
|
|||||||
elif self.options.level_gate_costs.value == 1:
|
elif self.options.level_gate_costs.value == 1:
|
||||||
gate_cost_mult = 0.8
|
gate_cost_mult = 0.8
|
||||||
|
|
||||||
shuffled_region_list = list(range(30))
|
self.shuffled_region_list = list(range(30))
|
||||||
emblem_requirement_list = list()
|
emblem_requirement_list = list()
|
||||||
self.multiworld.random.shuffle(shuffled_region_list)
|
self.multiworld.random.shuffle(self.shuffled_region_list)
|
||||||
levels_per_gate = self.get_levels_per_gate()
|
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
|
levels_added_to_gate = 0
|
||||||
total_levels_added = 0
|
total_levels_added = 0
|
||||||
current_gate = 0
|
current_gate = 0
|
||||||
@@ -250,11 +311,11 @@ class SA2BWorld(World):
|
|||||||
gates = list()
|
gates = list()
|
||||||
gates.append(LevelGate(0))
|
gates.append(LevelGate(0))
|
||||||
for i in range(30):
|
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)
|
emblem_requirement_list.append(current_gate_emblems)
|
||||||
levels_added_to_gate += 1
|
levels_added_to_gate += 1
|
||||||
total_levels_added += 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
|
current_gate += 1
|
||||||
if current_gate > self.options.number_of_level_gates.value:
|
if current_gate > self.options.number_of_level_gates.value:
|
||||||
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
|
self.gate_costs[current_gate] = current_gate_emblems
|
||||||
levels_added_to_gate = 0
|
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)
|
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)
|
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 = 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)]
|
itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)]
|
||||||
|
|
||||||
non_required_emblems = (total_emblem_count - 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))
|
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
|
# Carve Traps out of junk_count
|
||||||
trap_weights = []
|
trap_weights = []
|
||||||
@@ -291,7 +353,22 @@ class SA2BWorld(World):
|
|||||||
trap_weights += ([ItemName.slow_trap] * self.options.slow_trap_weight.value)
|
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.cutscene_trap] * self.options.cutscene_trap_weight.value)
|
||||||
trap_weights += ([ItemName.reverse_trap] * self.options.reverse_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.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
|
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))
|
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:
|
def create_item(self, name: str, force_classification=None, goal=0) -> Item:
|
||||||
data = item_table[name]
|
data = None
|
||||||
|
if name in event_table:
|
||||||
|
data = event_table[name]
|
||||||
|
else:
|
||||||
|
data = item_table[name]
|
||||||
|
|
||||||
if force_non_progression:
|
if force_classification is not None:
|
||||||
classification = ItemClassification.filler
|
classification = force_classification
|
||||||
elif name == ItemName.emblem or \
|
elif name == ItemName.emblem or \
|
||||||
name in emeralds_table.keys() or \
|
name in emeralds_table.keys() or \
|
||||||
(name == ItemName.knuckles_shovel_claws and goal in [4, 5]):
|
(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)
|
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):
|
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]:
|
if self.options.number_of_level_gates.value > 0 or self.options.goal.value in [4, 5, 6]:
|
||||||
spoiler_handle.write("\n")
|
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])
|
header_text = header_text.format(self.multiworld.player_name[self.player])
|
||||||
spoiler_handle.write(header_text)
|
spoiler_handle.write(header_text)
|
||||||
|
|
||||||
@@ -435,20 +523,20 @@ class SA2BWorld(World):
|
|||||||
continue
|
continue
|
||||||
level_region = exit.connected_region
|
level_region = exit.connected_region
|
||||||
for location in level_region.locations:
|
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):
|
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)
|
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)
|
er_hint_data[location.address] = str(self.black_market_costs[i]) + " " + str(ItemName.market_token)
|
||||||
|
|
||||||
|
|
||||||
hint_data[self.player] = er_hint_data
|
hint_data[self.player] = er_hint_data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations):
|
def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||||
if multiworld.get_game_players("Sonic Adventure 2 Battle"):
|
if multiworld.get_game_players("Sonic Adventure 2 Battle"):
|
||||||
progitempool.sort(
|
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:
|
def get_levels_per_gate(self) -> list:
|
||||||
levels_per_gate = list()
|
levels_per_gate = list()
|
||||||
@@ -486,6 +574,39 @@ class SA2BWorld(World):
|
|||||||
|
|
||||||
return levels_per_gate
|
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:
|
def any_chao_locations_active(self) -> bool:
|
||||||
if self.options.chao_race_difficulty.value > 0 or \
|
if self.options.chao_race_difficulty.value > 0 or \
|
||||||
self.options.chao_karate_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_choice = self.random.choice(valid_kindergarten_exits)
|
||||||
exit_room = exit_to_room_map[exit_choice]
|
exit_room = exit_to_room_map[exit_choice]
|
||||||
all_exits_copy.remove(exit_choice)
|
all_exits_copy.remove(exit_choice)
|
||||||
multi_rooms_copy.remove(exit_room)
|
|
||||||
|
|
||||||
destination = 0x06
|
destination = 0x06
|
||||||
single_rooms_copy.remove(destination)
|
single_rooms_copy.remove(destination)
|
||||||
@@ -723,7 +843,8 @@ class SA2BWorld(World):
|
|||||||
|
|
||||||
er_layout[exit_choice] = destination
|
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
|
er_layout[reverse_exit] = exit_room
|
||||||
|
|
||||||
|
|||||||
@@ -1625,6 +1625,6 @@ def get_location_offset(mission_id):
|
|||||||
|
|
||||||
|
|
||||||
def launch():
|
def launch():
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# StarCraft 2
|
# StarCraft 2
|
||||||
|
|
||||||
## Game page in other languages:
|
## Game page in other languages:
|
||||||
|
|
||||||
* [Français](/games/Starcraft%202/info/fr)
|
* [Français](/games/Starcraft%202/info/fr)
|
||||||
|
|
||||||
## What does randomization do to this game?
|
## What does randomization do to this game?
|
||||||
|
|
||||||
|
### Items and locations
|
||||||
|
|
||||||
The following unlocks are randomized as items:
|
The following unlocks are randomized as items:
|
||||||
1. Your ability to build any non-worker unit.
|
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
|
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.
|
notified via a text box in the top-right corner of the game screen.
|
||||||
Item unlocks are also logged in the Archipelago client.
|
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.
|
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.
|
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.
|
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?
|
## What is the goal of this game when randomized?
|
||||||
|
|
||||||
The goal is to beat the final mission in the mission order.
|
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
|
The yaml configuration file controls the mission order, which combination of the four StarCraft 2 campaigns can be
|
||||||
StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled.
|
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
|
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.
|
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?
|
## 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
|
* `/game_speed [game_speed]` Overrides the game speed for the world
|
||||||
* Options: default, slower, slow, normal, fast, faster
|
* Options: default, slower, slow, normal, fast, faster
|
||||||
* `/color [faction] [color]` Changes your color for one of your playable factions.
|
* `/color [faction] [color]` Changes your color for one of your playable factions.
|
||||||
* Faction options: raynor, kerrigan, primal, protoss, nova
|
* Run without arguments to list all factions and colors that are available.
|
||||||
* Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen,
|
|
||||||
brown, lightgreen, darkgrey, pink, rainbow, random, default
|
|
||||||
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
|
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
|
||||||
* Run without arguments to list all options.
|
* Run without arguments to list all options.
|
||||||
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource
|
* 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.
|
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.
|
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
|
## Known issues
|
||||||
|
|
||||||
- StarCraft 2 Archipelago does not support loading a saved game.
|
- 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.
|
To restart a mission, use the StarCraft 2 Client.
|
||||||
- A crash report is often generated when a mission is closed.
|
- A crash report is often generated when a mission is closed.
|
||||||
This does not affect the game and can be ignored.
|
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 ?
|
## 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
|
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:
|
le jeu:
|
||||||
1. La capacité de produire des unités, excepté les drones/probes/scv.
|
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*.
|
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.
|
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
|
Les missions peuvent être lancées par le client *StarCraft 2 Archipelago*, via l'interface graphique de l'onglet
|
||||||
*StarCraft 2 Launcher*.
|
*StarCraft 2 Launcher*.
|
||||||
Les segments qui se passent sur l'*Hyperion*, un Léviathan et la *Spear of Adun* ne sont pas inclus.
|
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*.
|
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*?
|
## 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 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
|
Le fichier de configuration yaml permet de spécifier le *mission order*, quelle combinaison des quatre campagnes de
|
||||||
*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le
|
*StarCraft 2* peuvent être utilisée et comment les missions sont distribuées dans le *mission order*.
|
||||||
*mission order*.
|
|
||||||
Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles
|
É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.
|
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*
|
## 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.
|
* `/game_speed [game_speed]` Remplace la vitesse du jeu pour le monde.
|
||||||
* Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*.
|
* Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*.
|
||||||
* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable.
|
* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable.
|
||||||
* Les options de *faction*: raynor, kerrigan, primal, protoss, nova.
|
* Si la commande est lancée sans option, la liste des *factions* et des couleurs disponibles sera affichée.
|
||||||
* Les options de couleur: *white*, *red*, *blue*, *teal*, *purple*, *yellow*, *orange*, *green*, *lightpink*,
|
|
||||||
*violet*, *lightgrey*, *darkgreen*, *brown*, *lightgreen*, *darkgrey*, *pink*, *rainbow*, *random*, *default*.
|
|
||||||
* `/option [option_name] [option_value]` Permet de changer un option normalement définit dans le *yaml*.
|
* `/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.
|
* 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
|
* 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.
|
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.
|
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
|
## Problèmes connus
|
||||||
|
|
||||||
- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde.
|
- *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*.
|
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.
|
- 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é.
|
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.
|
Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information.
|
||||||
|
|
||||||
### Common yaml questions
|
### Common yaml questions
|
||||||
|
|
||||||
#### How do I know I set my yaml up correctly?
|
#### How do I know I set my yaml up correctly?
|
||||||
|
|
||||||
The simplest way to check is to use the website [validator](/check).
|
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).
|
veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml).
|
||||||
|
|
||||||
### Questions récurrentes à propos du fichier *yaml*
|
### Questions récurrentes à propos du fichier *yaml*
|
||||||
|
|
||||||
#### Comment est-ce que je sais que mon *yaml* est bien défini?
|
#### 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
|
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):
|
def setup_victory(self):
|
||||||
if self.options.goal == Goal.option_community_center:
|
if self.options.goal == Goal.option_community_center:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_grandpa_evaluation:
|
elif self.options.goal == Goal.option_grandpa_evaluation:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_bottom_of_the_mines:
|
elif self.options.goal == Goal.option_bottom_of_the_mines:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_cryptic_note:
|
elif self.options.goal == Goal.option_cryptic_note:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_master_angler:
|
elif self.options.goal == Goal.option_master_angler:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_complete_collection:
|
elif self.options.goal == Goal.option_complete_collection:
|
||||||
self.create_event_location(location_table[GoalName.complete_museum],
|
self.create_event_location(location_table[GoalName.complete_museum],
|
||||||
self.logic.museum.can_complete_museum(),
|
self.logic.goal.can_complete_complete_collection(),
|
||||||
Event.victory)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_full_house:
|
elif self.options.goal == Goal.option_full_house:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_greatest_walnut_hunter:
|
elif self.options.goal == Goal.option_greatest_walnut_hunter:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_protector_of_the_valley:
|
elif self.options.goal == Goal.option_protector_of_the_valley:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_full_shipment:
|
elif self.options.goal == Goal.option_full_shipment:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_gourmet_chef:
|
elif self.options.goal == Goal.option_gourmet_chef:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_craft_master:
|
elif self.options.goal == Goal.option_craft_master:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_legend:
|
elif self.options.goal == Goal.option_legend:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_mystery_of_the_stardrops:
|
elif self.options.goal == Goal.option_mystery_of_the_stardrops:
|
||||||
self.create_event_location(location_table[GoalName.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)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_allsanity:
|
elif self.options.goal == Goal.option_allsanity:
|
||||||
self.create_event_location(location_table[GoalName.allsanity],
|
self.create_event_location(location_table[GoalName.allsanity],
|
||||||
HasProgressionPercent(self.player, 100),
|
self.logic.goal.can_complete_allsanity(),
|
||||||
Event.victory)
|
Event.victory)
|
||||||
elif self.options.goal == Goal.option_perfection:
|
elif self.options.goal == Goal.option_perfection:
|
||||||
self.create_event_location(location_table[GoalName.perfection],
|
self.create_event_location(location_table[GoalName.perfection],
|
||||||
HasProgressionPercent(self.player, 100),
|
self.logic.goal.can_complete_perfection(),
|
||||||
Event.victory)
|
Event.victory)
|
||||||
|
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(Event.victory, self.player)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has(Event.victory, self.player)
|
||||||
|
|||||||
@@ -13,12 +13,9 @@ from .relationship_logic import RelationshipLogicMixin
|
|||||||
from .season_logic import SeasonLogicMixin
|
from .season_logic import SeasonLogicMixin
|
||||||
from .skill_logic import SkillLogicMixin
|
from .skill_logic import SkillLogicMixin
|
||||||
from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSource, FriendshipSource, \
|
from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSource, FriendshipSource, \
|
||||||
QueenOfSauceSource, CookingRecipe, ShopFriendshipSource, \
|
QueenOfSauceSource, CookingRecipe, ShopFriendshipSource
|
||||||
all_cooking_recipes_by_name
|
|
||||||
from ..data.recipe_source import CutsceneSource, ShopTradeSource
|
from ..data.recipe_source import CutsceneSource, ShopTradeSource
|
||||||
from ..locations import locations_by_tag, LocationTags
|
|
||||||
from ..options import Chefsanity
|
from ..options import Chefsanity
|
||||||
from ..options import ExcludeGingerIsland
|
|
||||||
from ..stardew_rule import StardewRule, True_, False_
|
from ..stardew_rule import StardewRule, True_, False_
|
||||||
from ..strings.region_names import LogicRegion
|
from ..strings.region_names import LogicRegion
|
||||||
from ..strings.skill_names import Skill
|
from ..strings.skill_names import Skill
|
||||||
@@ -92,17 +89,3 @@ BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]
|
|||||||
@cache_self1
|
@cache_self1
|
||||||
def received_recipe(self, meal_name: str):
|
def received_recipe(self, meal_name: str):
|
||||||
return self.logic.received(f"{meal_name} Recipe")
|
return self.logic.received(f"{meal_name} Recipe")
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def can_cook_everything(self) -> StardewRule:
|
|
||||||
cooksanity_prefix = "Cook "
|
|
||||||
all_recipes_names = []
|
|
||||||
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
|
||||||
for location in locations_by_tag[LocationTags.COOKSANITY]:
|
|
||||||
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
|
||||||
continue
|
|
||||||
if location.mod_name and location.mod_name not in self.options.mods:
|
|
||||||
continue
|
|
||||||
all_recipes_names.append(location.name[len(cooksanity_prefix):])
|
|
||||||
all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
|
||||||
return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes))
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from functools import cached_property
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from Utils import cache_self1
|
from Utils import cache_self1
|
||||||
@@ -12,11 +11,10 @@ from .relationship_logic import RelationshipLogicMixin
|
|||||||
from .skill_logic import SkillLogicMixin
|
from .skill_logic import SkillLogicMixin
|
||||||
from .special_order_logic import SpecialOrderLogicMixin
|
from .special_order_logic import SpecialOrderLogicMixin
|
||||||
from .. import options
|
from .. import options
|
||||||
from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name
|
from ..data.craftable_data import CraftingRecipe
|
||||||
from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
|
from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
|
||||||
FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource
|
FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource
|
||||||
from ..locations import locations_by_tag, LocationTags
|
from ..options import Craftsanity, SpecialOrderLocations
|
||||||
from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland
|
|
||||||
from ..stardew_rule import StardewRule, True_, False_
|
from ..stardew_rule import StardewRule, True_, False_
|
||||||
from ..strings.region_names import Region
|
from ..strings.region_names import Region
|
||||||
|
|
||||||
@@ -71,7 +69,8 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
|
|||||||
if isinstance(recipe.source, ShopSource):
|
if isinstance(recipe.source, ShopSource):
|
||||||
return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price)
|
return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price)
|
||||||
if isinstance(recipe.source, SkillCraftsanitySource):
|
if isinstance(recipe.source, SkillCraftsanitySource):
|
||||||
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill, recipe.source.level)
|
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill,
|
||||||
|
recipe.source.level)
|
||||||
if isinstance(recipe.source, SkillSource):
|
if isinstance(recipe.source, SkillSource):
|
||||||
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level)
|
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level)
|
||||||
if isinstance(recipe.source, MasterySource):
|
if isinstance(recipe.source, MasterySource):
|
||||||
@@ -95,23 +94,3 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
|
|||||||
@cache_self1
|
@cache_self1
|
||||||
def received_recipe(self, item_name: str):
|
def received_recipe(self, item_name: str):
|
||||||
return self.logic.received(f"{item_name} Recipe")
|
return self.logic.received(f"{item_name} Recipe")
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def can_craft_everything(self) -> StardewRule:
|
|
||||||
craftsanity_prefix = "Craft "
|
|
||||||
all_recipes_names = []
|
|
||||||
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
|
||||||
exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled
|
|
||||||
for location in locations_by_tag[LocationTags.CRAFTSANITY]:
|
|
||||||
if not location.name.startswith(craftsanity_prefix):
|
|
||||||
continue
|
|
||||||
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
|
||||||
continue
|
|
||||||
# FIXME Remove when recipes are in content packs
|
|
||||||
if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags:
|
|
||||||
continue
|
|
||||||
if location.mod_name and location.mod_name not in self.options.mods:
|
|
||||||
continue
|
|
||||||
all_recipes_names.append(location.name[len(craftsanity_prefix):])
|
|
||||||
all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
|
||||||
return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes))
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class FishingLogicMixin(BaseLogicMixin):
|
|||||||
|
|
||||||
|
|
||||||
class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
|
class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
|
||||||
SkillLogicMixin]]):
|
SkillLogicMixin]]):
|
||||||
def can_fish_in_freshwater(self) -> StardewRule:
|
def can_fish_in_freshwater(self) -> StardewRule:
|
||||||
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
|
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
|
||||||
|
|
||||||
@@ -97,19 +97,5 @@ class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLog
|
|||||||
|
|
||||||
return self.logic.and_(*rules)
|
return self.logic.and_(*rules)
|
||||||
|
|
||||||
def can_catch_every_fish_for_fishsanity(self) -> StardewRule:
|
|
||||||
if not self.content.features.fishsanity.is_enabled:
|
|
||||||
return self.can_catch_every_fish()
|
|
||||||
|
|
||||||
rules = [self.has_max_fishing()]
|
|
||||||
|
|
||||||
rules.extend(
|
|
||||||
self.logic.fishing.can_catch_fish_for_fishsanity(fish)
|
|
||||||
for fish in self.content.fishes.values()
|
|
||||||
if self.content.features.fishsanity.is_included(fish)
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.logic.and_(*rules)
|
|
||||||
|
|
||||||
def has_specific_bait(self, fish: FishItem) -> StardewRule:
|
def has_specific_bait(self, fish: FishItem) -> StardewRule:
|
||||||
return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker)
|
return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker)
|
||||||
|
|||||||
173
worlds/stardew_valley/logic/goal_logic.py
Normal file
173
worlds/stardew_valley/logic/goal_logic.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from .base_logic import BaseLogic, BaseLogicMixin
|
||||||
|
from ..data.craftable_data import all_crafting_recipes_by_name
|
||||||
|
from ..data.recipe_data import all_cooking_recipes_by_name
|
||||||
|
from ..locations import LocationTags, locations_by_tag
|
||||||
|
from ..mods.mod_data import ModNames
|
||||||
|
from ..options import options
|
||||||
|
from ..stardew_rule import StardewRule
|
||||||
|
from ..strings.building_names import Building
|
||||||
|
from ..strings.quest_names import Quest
|
||||||
|
from ..strings.season_names import Season
|
||||||
|
from ..strings.wallet_item_names import Wallet
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from .logic import StardewLogic
|
||||||
|
else:
|
||||||
|
StardewLogic = object
|
||||||
|
|
||||||
|
|
||||||
|
class GoalLogicMixin(BaseLogicMixin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.goal = GoalLogic(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class GoalLogic(BaseLogic[StardewLogic]):
|
||||||
|
|
||||||
|
def can_complete_community_center(self) -> StardewRule:
|
||||||
|
return self.logic.bundle.can_complete_community_center
|
||||||
|
|
||||||
|
def can_finish_grandpa_evaluation(self) -> StardewRule:
|
||||||
|
# https://stardewvalleywiki.com/Grandpa
|
||||||
|
rules_worth_a_point = [
|
||||||
|
self.logic.money.can_have_earned_total(50_000),
|
||||||
|
self.logic.money.can_have_earned_total(100_000),
|
||||||
|
self.logic.money.can_have_earned_total(200_000),
|
||||||
|
self.logic.money.can_have_earned_total(300_000),
|
||||||
|
self.logic.money.can_have_earned_total(500_000),
|
||||||
|
self.logic.money.can_have_earned_total(1_000_000), # first point
|
||||||
|
self.logic.money.can_have_earned_total(1_000_000), # second point
|
||||||
|
self.logic.skill.has_total_level(30),
|
||||||
|
self.logic.skill.has_total_level(50),
|
||||||
|
self.logic.museum.can_complete_museum(),
|
||||||
|
# Catching every fish not expected
|
||||||
|
# Shipping every item not expected
|
||||||
|
self.logic.relationship.can_get_married() & self.logic.building.has_house(2),
|
||||||
|
self.logic.relationship.has_hearts_with_n(5, 8), # 5 Friends
|
||||||
|
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
|
||||||
|
self.logic.pet.has_pet_hearts(5), # Max Pet
|
||||||
|
self.logic.bundle.can_complete_community_center, # 1 point for Community Center Completion
|
||||||
|
self.logic.bundle.can_complete_community_center, # Ceremony first point
|
||||||
|
self.logic.bundle.can_complete_community_center, # Ceremony second point
|
||||||
|
self.logic.received(Wallet.skull_key),
|
||||||
|
self.logic.wallet.has_rusty_key(),
|
||||||
|
]
|
||||||
|
return self.logic.count(12, *rules_worth_a_point)
|
||||||
|
|
||||||
|
def can_complete_bottom_of_the_mines(self) -> StardewRule:
|
||||||
|
# The location is in the bottom of the mines region, so no actual rule is required
|
||||||
|
return self.logic.true_
|
||||||
|
|
||||||
|
def can_complete_cryptic_note(self) -> StardewRule:
|
||||||
|
return self.logic.quest.can_complete_quest(Quest.cryptic_note)
|
||||||
|
|
||||||
|
def can_complete_master_angler(self) -> StardewRule:
|
||||||
|
if not self.content.features.fishsanity.is_enabled:
|
||||||
|
return self.logic.fishing.can_catch_every_fish()
|
||||||
|
|
||||||
|
rules = [self.logic.fishing.has_max_fishing()]
|
||||||
|
|
||||||
|
rules.extend(
|
||||||
|
self.logic.fishing.can_catch_fish_for_fishsanity(fish)
|
||||||
|
for fish in self.content.fishes.values()
|
||||||
|
if self.content.features.fishsanity.is_included(fish)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.logic.and_(*rules)
|
||||||
|
|
||||||
|
def can_complete_complete_collection(self) -> StardewRule:
|
||||||
|
return self.logic.museum.can_complete_museum()
|
||||||
|
|
||||||
|
def can_complete_full_house(self) -> StardewRule:
|
||||||
|
return self.logic.relationship.has_children(2) & self.logic.relationship.can_reproduce()
|
||||||
|
|
||||||
|
def can_complete_greatest_walnut_hunter(self) -> StardewRule:
|
||||||
|
return self.logic.walnut.has_walnut(130)
|
||||||
|
|
||||||
|
def can_complete_protector_of_the_valley(self) -> StardewRule:
|
||||||
|
return self.logic.monster.can_complete_all_monster_slaying_goals()
|
||||||
|
|
||||||
|
def can_complete_full_shipment(self, all_location_names_in_slot: list[str]) -> StardewRule:
|
||||||
|
if self.options.shipsanity == options.Shipsanity.option_none:
|
||||||
|
return self.logic.shipping.can_ship_everything()
|
||||||
|
|
||||||
|
rules = [self.logic.building.has_building(Building.shipping_bin)]
|
||||||
|
|
||||||
|
for shipsanity_location in locations_by_tag[LocationTags.SHIPSANITY]:
|
||||||
|
if shipsanity_location.name not in all_location_names_in_slot:
|
||||||
|
continue
|
||||||
|
rules.append(self.logic.region.can_reach_location(shipsanity_location.name))
|
||||||
|
return self.logic.and_(*rules)
|
||||||
|
|
||||||
|
def can_complete_gourmet_chef(self) -> StardewRule:
|
||||||
|
cooksanity_prefix = "Cook "
|
||||||
|
all_recipes_names = []
|
||||||
|
exclude_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true
|
||||||
|
for location in locations_by_tag[LocationTags.COOKSANITY]:
|
||||||
|
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
||||||
|
continue
|
||||||
|
if location.mod_name and location.mod_name not in self.options.mods:
|
||||||
|
continue
|
||||||
|
all_recipes_names.append(location.name[len(cooksanity_prefix):])
|
||||||
|
all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
||||||
|
return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes))
|
||||||
|
|
||||||
|
def can_complete_craft_master(self) -> StardewRule:
|
||||||
|
craftsanity_prefix = "Craft "
|
||||||
|
all_recipes_names = []
|
||||||
|
exclude_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true
|
||||||
|
exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled
|
||||||
|
for location in locations_by_tag[LocationTags.CRAFTSANITY]:
|
||||||
|
if not location.name.startswith(craftsanity_prefix):
|
||||||
|
continue
|
||||||
|
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
||||||
|
continue
|
||||||
|
# FIXME Remove when recipes are in content packs
|
||||||
|
if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags:
|
||||||
|
continue
|
||||||
|
if location.mod_name and location.mod_name not in self.options.mods:
|
||||||
|
continue
|
||||||
|
all_recipes_names.append(location.name[len(craftsanity_prefix):])
|
||||||
|
all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
||||||
|
return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes))
|
||||||
|
|
||||||
|
def can_complete_legend(self) -> StardewRule:
|
||||||
|
return self.logic.money.can_have_earned_total(10_000_000)
|
||||||
|
|
||||||
|
def can_complete_mystery_of_the_stardrop(self) -> StardewRule:
|
||||||
|
other_rules = []
|
||||||
|
number_of_stardrops_to_receive = 0
|
||||||
|
number_of_stardrops_to_receive += 1 # The Mines level 100
|
||||||
|
number_of_stardrops_to_receive += 1 # Old Master Cannoli
|
||||||
|
number_of_stardrops_to_receive += 1 # Museum Stardrop
|
||||||
|
number_of_stardrops_to_receive += 1 # Krobus Stardrop
|
||||||
|
|
||||||
|
# Master Angler Stardrop
|
||||||
|
if self.content.features.fishsanity.is_enabled:
|
||||||
|
number_of_stardrops_to_receive += 1
|
||||||
|
else:
|
||||||
|
other_rules.append(self.logic.fishing.can_catch_every_fish())
|
||||||
|
|
||||||
|
if self.options.festival_locations == options.FestivalLocations.option_disabled: # Fair Stardrop
|
||||||
|
other_rules.append(self.logic.season.has(Season.fall))
|
||||||
|
else:
|
||||||
|
number_of_stardrops_to_receive += 1
|
||||||
|
|
||||||
|
# Spouse Stardrop
|
||||||
|
if self.content.features.friendsanity.is_enabled:
|
||||||
|
number_of_stardrops_to_receive += 1
|
||||||
|
else:
|
||||||
|
other_rules.append(self.logic.relationship.has_hearts_with_any_bachelor(13))
|
||||||
|
|
||||||
|
if ModNames.deepwoods in self.options.mods: # Petting the Unicorn
|
||||||
|
number_of_stardrops_to_receive += 1
|
||||||
|
|
||||||
|
return self.logic.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules, allow_empty=True)
|
||||||
|
|
||||||
|
def can_complete_allsanity(self) -> StardewRule:
|
||||||
|
return self.logic.has_progress_percent(100)
|
||||||
|
|
||||||
|
def can_complete_perfection(self) -> StardewRule:
|
||||||
|
return self.logic.has_progress_percent(100)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from .base_logic import BaseLogic
|
from .base_logic import BaseLogic
|
||||||
from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_
|
from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_, HasProgressionPercent
|
||||||
|
|
||||||
|
|
||||||
class HasLogicMixin(BaseLogic[None]):
|
class HasLogicMixin(BaseLogic[None]):
|
||||||
@@ -23,6 +23,12 @@ class HasLogicMixin(BaseLogic[None]):
|
|||||||
def has_n(self, *items: str, count: int):
|
def has_n(self, *items: str, count: int):
|
||||||
return self.count(count, *(self.has(item) for item in items))
|
return self.count(count, *(self.has(item) for item in items))
|
||||||
|
|
||||||
|
def has_progress_percent(self, percent: int):
|
||||||
|
assert percent >= 0, "Can't have a negative progress percent"
|
||||||
|
assert percent <= 100, "Can't have a progress percent over 100"
|
||||||
|
|
||||||
|
return HasProgressionPercent(self.player, percent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def count(count: int, *rules: StardewRule) -> StardewRule:
|
def count(count: int, *rules: StardewRule) -> StardewRule:
|
||||||
assert rules, "Can't create a Count conditions without rules"
|
assert rules, "Can't create a Count conditions without rules"
|
||||||
@@ -47,8 +53,14 @@ class HasLogicMixin(BaseLogic[None]):
|
|||||||
return Count(rules, count)
|
return Count(rules, count)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def and_(*rules: StardewRule) -> StardewRule:
|
def and_(*rules: StardewRule, allow_empty: bool = False) -> StardewRule:
|
||||||
assert rules, "Can't create a And conditions without rules"
|
"""
|
||||||
|
:param rules: The rules to combine
|
||||||
|
:param allow_empty: If True, return true_ when no rules are given. Otherwise, raise an error.
|
||||||
|
"""
|
||||||
|
if not rules:
|
||||||
|
assert allow_empty, "Can't create a And conditions without rules"
|
||||||
|
return true_
|
||||||
|
|
||||||
if len(rules) == 1:
|
if len(rules) == 1:
|
||||||
return rules[0]
|
return rules[0]
|
||||||
@@ -56,8 +68,14 @@ class HasLogicMixin(BaseLogic[None]):
|
|||||||
return And(*rules)
|
return And(*rules)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def or_(*rules: StardewRule) -> StardewRule:
|
def or_(*rules: StardewRule, allow_empty: bool = False) -> StardewRule:
|
||||||
assert rules, "Can't create a Or conditions without rules"
|
"""
|
||||||
|
:param rules: The rules to combine
|
||||||
|
:param allow_empty: If True, return false_ when no rules are given. Otherwise, raise an error.
|
||||||
|
"""
|
||||||
|
if not rules:
|
||||||
|
assert allow_empty, "Can't create a Or conditions without rules"
|
||||||
|
return false_
|
||||||
|
|
||||||
if len(rules) == 1:
|
if len(rules) == 1:
|
||||||
return rules[0]
|
return rules[0]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .farming_logic import FarmingLogicMixin
|
|||||||
from .festival_logic import FestivalLogicMixin
|
from .festival_logic import FestivalLogicMixin
|
||||||
from .fishing_logic import FishingLogicMixin
|
from .fishing_logic import FishingLogicMixin
|
||||||
from .gift_logic import GiftLogicMixin
|
from .gift_logic import GiftLogicMixin
|
||||||
|
from .goal_logic import GoalLogicMixin
|
||||||
from .grind_logic import GrindLogicMixin
|
from .grind_logic import GrindLogicMixin
|
||||||
from .harvesting_logic import HarvestingLogicMixin
|
from .harvesting_logic import HarvestingLogicMixin
|
||||||
from .has_logic import HasLogicMixin
|
from .has_logic import HasLogicMixin
|
||||||
@@ -50,8 +51,7 @@ from ..data.museum_data import all_museum_items
|
|||||||
from ..data.recipe_data import all_cooking_recipes
|
from ..data.recipe_data import all_cooking_recipes
|
||||||
from ..mods.logic.magic_logic import MagicLogicMixin
|
from ..mods.logic.magic_logic import MagicLogicMixin
|
||||||
from ..mods.logic.mod_logic import ModLogicMixin
|
from ..mods.logic.mod_logic import ModLogicMixin
|
||||||
from ..mods.mod_data import ModNames
|
from ..options import ExcludeGingerIsland, StardewValleyOptions
|
||||||
from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions
|
|
||||||
from ..stardew_rule import False_, True_, StardewRule
|
from ..stardew_rule import False_, True_, StardewRule
|
||||||
from ..strings.animal_names import Animal
|
from ..strings.animal_names import Animal
|
||||||
from ..strings.animal_product_names import AnimalProduct
|
from ..strings.animal_product_names import AnimalProduct
|
||||||
@@ -93,7 +93,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
|||||||
CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin,
|
CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin,
|
||||||
SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
|
SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
|
||||||
SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin,
|
SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin,
|
||||||
RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin):
|
RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin, GoalLogicMixin):
|
||||||
player: int
|
player: int
|
||||||
options: StardewValleyOptions
|
options: StardewValleyOptions
|
||||||
content: StardewContent
|
content: StardewContent
|
||||||
@@ -375,71 +375,11 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
|||||||
def can_smelt(self, item: str) -> StardewRule:
|
def can_smelt(self, item: str) -> StardewRule:
|
||||||
return self.has(Machine.furnace) & self.has(item)
|
return self.has(Machine.furnace) & self.has(item)
|
||||||
|
|
||||||
def can_finish_grandpa_evaluation(self) -> StardewRule:
|
|
||||||
# https://stardewvalleywiki.com/Grandpa
|
|
||||||
rules_worth_a_point = [
|
|
||||||
self.money.can_have_earned_total(50000), # 50 000g
|
|
||||||
self.money.can_have_earned_total(100000), # 100 000g
|
|
||||||
self.money.can_have_earned_total(200000), # 200 000g
|
|
||||||
self.money.can_have_earned_total(300000), # 300 000g
|
|
||||||
self.money.can_have_earned_total(500000), # 500 000g
|
|
||||||
self.money.can_have_earned_total(1000000), # 1 000 000g first point
|
|
||||||
self.money.can_have_earned_total(1000000), # 1 000 000g second point
|
|
||||||
self.skill.has_total_level(30), # Total Skills: 30
|
|
||||||
self.skill.has_total_level(50), # Total Skills: 50
|
|
||||||
self.museum.can_complete_museum(), # Completing the museum for a point
|
|
||||||
# Catching every fish not expected
|
|
||||||
# Shipping every item not expected
|
|
||||||
self.relationship.can_get_married() & self.building.has_house(2),
|
|
||||||
self.relationship.has_hearts_with_n(5, 8), # 5 Friends
|
|
||||||
self.relationship.has_hearts_with_n(10, 8), # 10 friends
|
|
||||||
self.pet.has_pet_hearts(5), # Max Pet
|
|
||||||
self.bundle.can_complete_community_center, # Community Center Completion
|
|
||||||
self.bundle.can_complete_community_center, # CC Ceremony first point
|
|
||||||
self.bundle.can_complete_community_center, # CC Ceremony second point
|
|
||||||
self.received(Wallet.skull_key), # Skull Key obtained
|
|
||||||
self.wallet.has_rusty_key(), # Rusty key obtained
|
|
||||||
]
|
|
||||||
return self.count(12, *rules_worth_a_point)
|
|
||||||
|
|
||||||
def has_island_trader(self) -> StardewRule:
|
def has_island_trader(self) -> StardewRule:
|
||||||
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
|
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
|
||||||
return False_()
|
return False_()
|
||||||
return self.region.can_reach(Region.island_trader)
|
return self.region.can_reach(Region.island_trader)
|
||||||
|
|
||||||
def has_all_stardrops(self) -> StardewRule:
|
|
||||||
other_rules = []
|
|
||||||
number_of_stardrops_to_receive = 0
|
|
||||||
number_of_stardrops_to_receive += 1 # The Mines level 100
|
|
||||||
number_of_stardrops_to_receive += 1 # Old Master Cannoli
|
|
||||||
number_of_stardrops_to_receive += 1 # Museum Stardrop
|
|
||||||
number_of_stardrops_to_receive += 1 # Krobus Stardrop
|
|
||||||
|
|
||||||
# Master Angler Stardrop
|
|
||||||
if self.content.features.fishsanity.is_enabled:
|
|
||||||
number_of_stardrops_to_receive += 1
|
|
||||||
else:
|
|
||||||
other_rules.append(self.fishing.can_catch_every_fish())
|
|
||||||
|
|
||||||
if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop
|
|
||||||
other_rules.append(self.season.has(Season.fall))
|
|
||||||
else:
|
|
||||||
number_of_stardrops_to_receive += 1
|
|
||||||
|
|
||||||
# Spouse Stardrop
|
|
||||||
if self.content.features.friendsanity.is_enabled:
|
|
||||||
number_of_stardrops_to_receive += 1
|
|
||||||
else:
|
|
||||||
other_rules.append(self.relationship.has_hearts_with_any_bachelor(13))
|
|
||||||
|
|
||||||
if ModNames.deepwoods in self.options.mods: # Petting the Unicorn
|
|
||||||
number_of_stardrops_to_receive += 1
|
|
||||||
|
|
||||||
if not other_rules:
|
|
||||||
return self.received("Stardrop", number_of_stardrops_to_receive)
|
|
||||||
|
|
||||||
return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules)
|
|
||||||
|
|
||||||
def has_abandoned_jojamart(self) -> StardewRule:
|
def has_abandoned_jojamart(self) -> StardewRule:
|
||||||
return self.received(CommunityUpgrade.movie_theater, 1)
|
return self.received(CommunityUpgrade.movie_theater, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Union, List
|
from typing import Union
|
||||||
|
|
||||||
from Utils import cache_self1
|
from Utils import cache_self1
|
||||||
from .base_logic import BaseLogic, BaseLogicMixin
|
from .base_logic import BaseLogic, BaseLogicMixin
|
||||||
@@ -8,7 +8,7 @@ from .has_logic import HasLogicMixin
|
|||||||
from .received_logic import ReceivedLogicMixin
|
from .received_logic import ReceivedLogicMixin
|
||||||
from .region_logic import RegionLogicMixin
|
from .region_logic import RegionLogicMixin
|
||||||
from ..locations import LocationTags, locations_by_tag
|
from ..locations import LocationTags, locations_by_tag
|
||||||
from ..options import ExcludeGingerIsland, Shipsanity
|
from ..options import ExcludeGingerIsland
|
||||||
from ..options import SpecialOrderLocations
|
from ..options import SpecialOrderLocations
|
||||||
from ..stardew_rule import StardewRule
|
from ..stardew_rule import StardewRule
|
||||||
from ..strings.building_names import Building
|
from ..strings.building_names import Building
|
||||||
@@ -45,15 +45,3 @@ class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, Buil
|
|||||||
continue
|
continue
|
||||||
all_items_to_ship.append(location.name[len(shipsanity_prefix):])
|
all_items_to_ship.append(location.name[len(shipsanity_prefix):])
|
||||||
return self.logic.building.has_building(Building.shipping_bin) & self.logic.has_all(*all_items_to_ship)
|
return self.logic.building.has_building(Building.shipping_bin) & self.logic.has_all(*all_items_to_ship)
|
||||||
|
|
||||||
def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule:
|
|
||||||
if self.options.shipsanity == Shipsanity.option_none:
|
|
||||||
return self.logic.shipping.can_ship_everything()
|
|
||||||
|
|
||||||
rules = [self.logic.building.has_building(Building.shipping_bin)]
|
|
||||||
|
|
||||||
for shipsanity_location in locations_by_tag[LocationTags.SHIPSANITY]:
|
|
||||||
if shipsanity_location.name not in all_location_names_in_slot:
|
|
||||||
continue
|
|
||||||
rules.append(self.logic.region.can_reach_location(shipsanity_location.name))
|
|
||||||
return self.logic.and_(*rules)
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class WitnessWorld(World):
|
|||||||
item_name_groups = static_witness_items.ITEM_GROUPS
|
item_name_groups = static_witness_items.ITEM_GROUPS
|
||||||
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
|
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
|
||||||
|
|
||||||
required_client_version = (0, 5, 1)
|
required_client_version = (0, 6, 0)
|
||||||
|
|
||||||
player_logic: WitnessPlayerLogic
|
player_logic: WitnessPlayerLogic
|
||||||
player_locations: WitnessPlayerLocations
|
player_locations: WitnessPlayerLocations
|
||||||
|
|||||||
33
worlds/witness/data/definition_classes.py
Normal file
33
worlds/witness/data/definition_classes.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import FrozenSet, List, NamedTuple
|
||||||
|
|
||||||
|
# A WitnessRule is just an or-chain of and-conditions.
|
||||||
|
# It represents the set of all options that could fulfill this requirement.
|
||||||
|
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
|
||||||
|
# {} is an unusable requirement.
|
||||||
|
# {{}} is an always usable requirement.
|
||||||
|
WitnessRule = FrozenSet[FrozenSet[str]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AreaDefinition:
|
||||||
|
name: str
|
||||||
|
regions: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegionDefinition:
|
||||||
|
name: str
|
||||||
|
short_name: str
|
||||||
|
area: AreaDefinition
|
||||||
|
logical_entities: List[str] = field(default_factory=list)
|
||||||
|
physical_entities: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionDefinition(NamedTuple):
|
||||||
|
target_region: str
|
||||||
|
traversal_rule: WitnessRule
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_be_traversed(self) -> bool:
|
||||||
|
return bool(self.traversal_rule)
|
||||||
@@ -486,5 +486,5 @@ for key, item in ALL_LOCATIONS_TO_IDS.items():
|
|||||||
ALL_LOCATIONS_TO_ID[key] = item
|
ALL_LOCATIONS_TO_ID[key] = item
|
||||||
|
|
||||||
for loc in ALL_LOCATIONS_TO_IDS:
|
for loc in ALL_LOCATIONS_TO_IDS:
|
||||||
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"]
|
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"].name
|
||||||
AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc)
|
AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
from typing import Any, Dict, FrozenSet, List, Optional, Set
|
||||||
|
|
||||||
from Utils import cache_argsless
|
from Utils import cache_argsless
|
||||||
|
|
||||||
|
from .definition_classes import AreaDefinition, ConnectionDefinition, RegionDefinition, WitnessRule
|
||||||
from .item_definition_classes import (
|
from .item_definition_classes import (
|
||||||
CATEGORY_NAME_MAPPINGS,
|
CATEGORY_NAME_MAPPINGS,
|
||||||
DoorItemDefinition,
|
DoorItemDefinition,
|
||||||
@@ -13,7 +14,6 @@ from .item_definition_classes import (
|
|||||||
)
|
)
|
||||||
from .settings.easter_eggs import EASTER_EGGS
|
from .settings.easter_eggs import EASTER_EGGS
|
||||||
from .utils import (
|
from .utils import (
|
||||||
WitnessRule,
|
|
||||||
define_new_region,
|
define_new_region,
|
||||||
get_items,
|
get_items,
|
||||||
get_sigma_expert_logic,
|
get_sigma_expert_logic,
|
||||||
@@ -21,7 +21,7 @@ from .utils import (
|
|||||||
get_umbra_variety_logic,
|
get_umbra_variety_logic,
|
||||||
get_vanilla_logic,
|
get_vanilla_logic,
|
||||||
logical_or_witness_rules,
|
logical_or_witness_rules,
|
||||||
parse_lambda,
|
parse_witness_rule,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ class StaticWitnessLogicObj:
|
|||||||
lines = get_sigma_normal_logic()
|
lines = get_sigma_normal_logic()
|
||||||
|
|
||||||
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
|
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
|
||||||
self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
self.ALL_REGIONS_BY_NAME: Dict[str, RegionDefinition] = {}
|
||||||
self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
self.ALL_AREAS_BY_NAME: Dict[str, AreaDefinition] = {}
|
||||||
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set))
|
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, List[ConnectionDefinition]] = defaultdict(list)
|
||||||
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {}
|
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, List[ConnectionDefinition]] = {}
|
||||||
|
|
||||||
self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
|
self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
|
||||||
self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
||||||
@@ -55,15 +55,15 @@ class StaticWitnessLogicObj:
|
|||||||
area_counts: Dict[str, int] = Counter()
|
area_counts: Dict[str, int] = Counter()
|
||||||
for region_name, entity_amount in EASTER_EGGS.items():
|
for region_name, entity_amount in EASTER_EGGS.items():
|
||||||
region_object = self.ALL_REGIONS_BY_NAME[region_name]
|
region_object = self.ALL_REGIONS_BY_NAME[region_name]
|
||||||
correct_area = region_object["area"]
|
correct_area = region_object.area
|
||||||
|
|
||||||
for _ in range(entity_amount):
|
for _ in range(entity_amount):
|
||||||
location_id = 160200 + egg_counter
|
location_id = 160200 + egg_counter
|
||||||
entity_hex = hex(0xEE000 + egg_counter)
|
entity_hex = hex(0xEE000 + egg_counter)
|
||||||
egg_counter += 1
|
egg_counter += 1
|
||||||
|
|
||||||
area_counts[correct_area["name"]] += 1
|
area_counts[correct_area.name] += 1
|
||||||
full_entity_name = f"{correct_area['name']} Easter Egg {area_counts[correct_area['name']]}"
|
full_entity_name = f"{correct_area.name} Easter Egg {area_counts[correct_area.name]}"
|
||||||
|
|
||||||
self.ENTITIES_BY_HEX[entity_hex] = {
|
self.ENTITIES_BY_HEX[entity_hex] = {
|
||||||
"checkName": full_entity_name,
|
"checkName": full_entity_name,
|
||||||
@@ -81,11 +81,11 @@ class StaticWitnessLogicObj:
|
|||||||
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
|
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
|
||||||
"entities": frozenset({frozenset({})})
|
"entities": frozenset({frozenset({})})
|
||||||
}
|
}
|
||||||
region_object["entities"].append(entity_hex)
|
region_object.logical_entities.append(entity_hex)
|
||||||
region_object["physical_entities"].append(entity_hex)
|
region_object.physical_entities.append(entity_hex)
|
||||||
|
|
||||||
easter_egg_region = self.ALL_REGIONS_BY_NAME["Easter Eggs"]
|
easter_egg_region = self.ALL_REGIONS_BY_NAME["Easter Eggs"]
|
||||||
easter_egg_area = easter_egg_region["area"]
|
easter_egg_area = easter_egg_region.area
|
||||||
for i in range(sum(EASTER_EGGS.values())):
|
for i in range(sum(EASTER_EGGS.values())):
|
||||||
location_id = 160000 + i
|
location_id = 160000 + i
|
||||||
entity_hex = hex(0xEE200 + i)
|
entity_hex = hex(0xEE200 + i)
|
||||||
@@ -111,19 +111,15 @@ class StaticWitnessLogicObj:
|
|||||||
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
|
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
|
||||||
"entities": frozenset({frozenset({})})
|
"entities": frozenset({frozenset({})})
|
||||||
}
|
}
|
||||||
easter_egg_region["entities"].append(entity_hex)
|
easter_egg_region.logical_entities.append(entity_hex)
|
||||||
easter_egg_region["physical_entities"].append(entity_hex)
|
easter_egg_region.physical_entities.append(entity_hex)
|
||||||
|
|
||||||
def read_logic_file(self, lines: List[str]) -> None:
|
def read_logic_file(self, lines: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Reads the logic file and does the initial population of data structures
|
Reads the logic file and does the initial population of data structures
|
||||||
"""
|
"""
|
||||||
|
current_area = AreaDefinition("Misc")
|
||||||
current_region = {}
|
current_region = RegionDefinition("Fake", "Fake", current_area) # Unused, but makes PyCharm & mypy shut up
|
||||||
current_area: Dict[str, Any] = {
|
|
||||||
"name": "Misc",
|
|
||||||
"regions": [],
|
|
||||||
}
|
|
||||||
self.ALL_AREAS_BY_NAME["Misc"] = current_area
|
self.ALL_AREAS_BY_NAME["Misc"] = current_area
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@@ -133,19 +129,16 @@ class StaticWitnessLogicObj:
|
|||||||
if line[-1] == ":":
|
if line[-1] == ":":
|
||||||
new_region_and_connections = define_new_region(line, current_area)
|
new_region_and_connections = define_new_region(line, current_area)
|
||||||
current_region = new_region_and_connections[0]
|
current_region = new_region_and_connections[0]
|
||||||
region_name = current_region["name"]
|
region_name = current_region.name
|
||||||
self.ALL_REGIONS_BY_NAME[region_name] = current_region
|
self.ALL_REGIONS_BY_NAME[region_name] = current_region
|
||||||
for connection in new_region_and_connections[1]:
|
for connection in new_region_and_connections[1]:
|
||||||
self.CONNECTIONS_WITH_DUPLICATES[region_name][connection[0]].add(connection[1])
|
self.CONNECTIONS_WITH_DUPLICATES[region_name].append(connection)
|
||||||
current_area["regions"].append(region_name)
|
current_area.regions.append(region_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line[0] == "=":
|
if line[0] == "=":
|
||||||
area_name = line[2:-2]
|
area_name = line[2:-2]
|
||||||
current_area = {
|
current_area = AreaDefinition(area_name, [])
|
||||||
"name": area_name,
|
|
||||||
"regions": [],
|
|
||||||
}
|
|
||||||
self.ALL_AREAS_BY_NAME[area_name] = current_area
|
self.ALL_AREAS_BY_NAME[area_name] = current_area
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -158,9 +151,9 @@ class StaticWitnessLogicObj:
|
|||||||
entity_hex = entity_name_full[0:7]
|
entity_hex = entity_name_full[0:7]
|
||||||
entity_name = entity_name_full[9:-1]
|
entity_name = entity_name_full[9:-1]
|
||||||
|
|
||||||
required_panel_lambda = line_split.pop(0)
|
entity_requirement_string = line_split.pop(0)
|
||||||
|
|
||||||
full_entity_name = current_region["shortName"] + " " + entity_name
|
full_entity_name = current_region.short_name + " " + entity_name
|
||||||
|
|
||||||
if location_id == "Door" or location_id == "Laser":
|
if location_id == "Door" or location_id == "Laser":
|
||||||
self.ENTITIES_BY_HEX[entity_hex] = {
|
self.ENTITIES_BY_HEX[entity_hex] = {
|
||||||
@@ -177,18 +170,18 @@ class StaticWitnessLogicObj:
|
|||||||
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
|
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
|
||||||
|
|
||||||
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
|
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
|
||||||
"entities": parse_lambda(required_panel_lambda)
|
"entities": parse_witness_rule(entity_requirement_string)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Lasers and Doors exist in a region, but don't have a regional *requirement*
|
# Lasers and Doors exist in a region, but don't have a regional *requirement*
|
||||||
# If a laser is activated, you don't need to physically walk up to it for it to count
|
# If a laser is activated, you don't need to physically walk up to it for it to count
|
||||||
# As such, logically, they behave more as if they were part of the "Entry" region
|
# As such, logically, they behave more as if they were part of the "Entry" region
|
||||||
self.ALL_REGIONS_BY_NAME["Entry"]["entities"].append(entity_hex)
|
self.ALL_REGIONS_BY_NAME["Entry"].logical_entities.append(entity_hex)
|
||||||
# However, it will also be important to keep track of their physical location for postgame purposes.
|
# However, it will also be important to keep track of their physical location for postgame purposes.
|
||||||
current_region["physical_entities"].append(entity_hex)
|
current_region.physical_entities.append(entity_hex)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
required_item_lambda = line_split.pop(0)
|
item_requirement_string = line_split.pop(0)
|
||||||
|
|
||||||
laser_names = {
|
laser_names = {
|
||||||
"Laser",
|
"Laser",
|
||||||
@@ -224,18 +217,18 @@ class StaticWitnessLogicObj:
|
|||||||
entity_type = "Panel"
|
entity_type = "Panel"
|
||||||
location_type = "General"
|
location_type = "General"
|
||||||
|
|
||||||
required_items = parse_lambda(required_item_lambda)
|
required_items = parse_witness_rule(item_requirement_string)
|
||||||
required_panels = parse_lambda(required_panel_lambda)
|
required_entities = parse_witness_rule(entity_requirement_string)
|
||||||
|
|
||||||
required_items = frozenset(required_items)
|
required_items = frozenset(required_items)
|
||||||
|
|
||||||
requirement = {
|
requirement = {
|
||||||
"entities": required_panels,
|
"entities": required_entities,
|
||||||
"items": required_items
|
"items": required_items
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity_type == "Obelisk Side":
|
if entity_type == "Obelisk Side":
|
||||||
eps = set(next(iter(required_panels)))
|
eps = set(next(iter(required_entities)))
|
||||||
eps -= {"Theater to Tunnels"}
|
eps -= {"Theater to Tunnels"}
|
||||||
|
|
||||||
eps_ints = {int(h, 16) for h in eps}
|
eps_ints = {int(h, 16) for h in eps}
|
||||||
@@ -260,39 +253,43 @@ class StaticWitnessLogicObj:
|
|||||||
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
|
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
|
||||||
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement
|
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement
|
||||||
|
|
||||||
current_region["entities"].append(entity_hex)
|
current_region.logical_entities.append(entity_hex)
|
||||||
current_region["physical_entities"].append(entity_hex)
|
current_region.physical_entities.append(entity_hex)
|
||||||
|
|
||||||
self.add_easter_eggs()
|
self.add_easter_eggs()
|
||||||
|
|
||||||
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None:
|
def reverse_connection(self, source_region: str, connection: ConnectionDefinition) -> None:
|
||||||
target = connection[0]
|
|
||||||
traversal_options = connection[1]
|
|
||||||
|
|
||||||
# Reverse this connection with all its possibilities, except the ones marked as "OneWay".
|
# Reverse this connection with all its possibilities, except the ones marked as "OneWay".
|
||||||
for requirement in traversal_options:
|
remaining_options: Set[FrozenSet[str]] = set()
|
||||||
remaining_options = set()
|
for sub_option in connection.traversal_rule:
|
||||||
for option in requirement:
|
if not any(req == "TrueOneWay" for req in sub_option):
|
||||||
if not any(req == "TrueOneWay" for req in option):
|
remaining_options.add(sub_option)
|
||||||
remaining_options.add(option)
|
|
||||||
|
|
||||||
if remaining_options:
|
reversed_connection = ConnectionDefinition(source_region, frozenset(remaining_options))
|
||||||
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
|
if reversed_connection.can_be_traversed:
|
||||||
|
self.CONNECTIONS_WITH_DUPLICATES[connection.target_region].append(reversed_connection)
|
||||||
|
|
||||||
def reverse_connections(self) -> None:
|
def reverse_connections(self) -> None:
|
||||||
# Iterate all connections
|
# Iterate all connections
|
||||||
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
|
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
|
||||||
for connection in connections.items():
|
for connection in connections:
|
||||||
self.reverse_connection(region_name, connection)
|
self.reverse_connection(region_name, connection)
|
||||||
|
|
||||||
def combine_connections(self) -> None:
|
def combine_connections(self) -> None:
|
||||||
# All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice.
|
# All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice.
|
||||||
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME}
|
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: [] for region_name in self.ALL_REGIONS_BY_NAME}
|
||||||
|
|
||||||
for source, connections in self.CONNECTIONS_WITH_DUPLICATES.items():
|
for source, connections in self.CONNECTIONS_WITH_DUPLICATES.items():
|
||||||
for target, requirement in connections.items():
|
# Organize rules by target region
|
||||||
combined_req = logical_or_witness_rules(requirement)
|
traversal_options_by_target_region = defaultdict(list)
|
||||||
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req))
|
for target_region, traversal_option in connections:
|
||||||
|
traversal_options_by_target_region[target_region].append(traversal_option)
|
||||||
|
|
||||||
|
# Combine connections to the same target region into one connection
|
||||||
|
for target, traversal_rules in traversal_options_by_target_region.items():
|
||||||
|
combined_rule = logical_or_witness_rules(traversal_rules)
|
||||||
|
combined_connection = ConnectionDefinition(target, combined_rule)
|
||||||
|
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].append(combined_connection)
|
||||||
|
|
||||||
|
|
||||||
# Item data parsed from WitnessItems.txt
|
# Item data parsed from WitnessItems.txt
|
||||||
|
|||||||
@@ -2,17 +2,12 @@ from datetime import date
|
|||||||
from math import floor
|
from math import floor
|
||||||
from pkgutil import get_data
|
from pkgutil import get_data
|
||||||
from random import Random
|
from random import Random
|
||||||
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
|
from typing import Collection, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
|
||||||
|
|
||||||
|
from .definition_classes import AreaDefinition, ConnectionDefinition, RegionDefinition, WitnessRule
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
# A WitnessRule is just an or-chain of and-conditions.
|
|
||||||
# It represents the set of all options that could fulfill this requirement.
|
|
||||||
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
|
|
||||||
# {} is an unusable requirement.
|
|
||||||
# {{}} is an always usable requirement.
|
|
||||||
WitnessRule = FrozenSet[FrozenSet[str]]
|
|
||||||
|
|
||||||
|
|
||||||
def cast_not_none(value: Optional[T]) -> T:
|
def cast_not_none(value: Optional[T]) -> T:
|
||||||
assert value is not None
|
assert value is not None
|
||||||
@@ -62,7 +57,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
|
|||||||
return rounded_output
|
return rounded_output
|
||||||
|
|
||||||
|
|
||||||
def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]:
|
def define_new_region(region_string: str, area: AreaDefinition) -> Tuple[RegionDefinition, List[ConnectionDefinition]]:
|
||||||
"""
|
"""
|
||||||
Returns a region object by parsing a line in the logic file
|
Returns a region object by parsing a line in the logic file
|
||||||
"""
|
"""
|
||||||
@@ -77,35 +72,28 @@ def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[st
|
|||||||
region_name = region_name_split[0]
|
region_name = region_name_split[0]
|
||||||
region_name_simple = region_name_split[1][:-1]
|
region_name_simple = region_name_split[1][:-1]
|
||||||
|
|
||||||
options = set()
|
options = []
|
||||||
|
|
||||||
for _ in range(len(line_split) // 2):
|
for _ in range(len(line_split) // 2):
|
||||||
connected_region = line_split.pop(0)
|
connected_region = line_split.pop(0)
|
||||||
corresponding_lambda = line_split.pop(0)
|
traversal_rule_string = line_split.pop(0)
|
||||||
|
|
||||||
options.add(
|
options.append(ConnectionDefinition(connected_region, parse_witness_rule(traversal_rule_string)))
|
||||||
(connected_region, parse_lambda(corresponding_lambda))
|
|
||||||
)
|
region_obj = RegionDefinition(region_name, region_name_simple, area)
|
||||||
|
|
||||||
region_obj = {
|
|
||||||
"name": region_name,
|
|
||||||
"shortName": region_name_simple,
|
|
||||||
"entities": [],
|
|
||||||
"physical_entities": [],
|
|
||||||
"area": area,
|
|
||||||
}
|
|
||||||
return region_obj, options
|
return region_obj, options
|
||||||
|
|
||||||
|
|
||||||
def parse_lambda(lambda_string: str) -> WitnessRule:
|
def parse_witness_rule(rule_string: str) -> WitnessRule:
|
||||||
"""
|
"""
|
||||||
Turns a lambda String literal like this: a | b & c
|
Turns a rule string literal like this: a | b & c
|
||||||
into a set of sets like this: {{a}, {b, c}}
|
into a set of sets (called "WitnessRule") like this: {{a}, {b, c}}
|
||||||
The lambda has to be in DNF.
|
The rule string has to be in DNF.
|
||||||
"""
|
"""
|
||||||
if lambda_string == "True":
|
if rule_string == "True":
|
||||||
return frozenset([frozenset()])
|
return frozenset([frozenset()])
|
||||||
split_ands = set(lambda_string.split(" | "))
|
split_ands = set(rule_string.split(" | "))
|
||||||
return frozenset({frozenset(a.split(" & ")) for a in split_ands})
|
return frozenset({frozenset(a.split(" & ")) for a in split_ands})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class EntityHuntPicker:
|
|||||||
|
|
||||||
eligible_panels_by_area = defaultdict(set)
|
eligible_panels_by_area = defaultdict(set)
|
||||||
for eligible_panel in all_eligible_panels:
|
for eligible_panel in all_eligible_panels:
|
||||||
associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"]["name"]
|
associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"].name
|
||||||
eligible_panels_by_area[associated_area].add(eligible_panel)
|
eligible_panels_by_area[associated_area].add(eligible_panel)
|
||||||
|
|
||||||
return all_eligible_panels, eligible_panels_by_area
|
return all_eligible_panels, eligible_panels_by_area
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if __name__ == "__main__":
|
|||||||
for entity_id, entity_object in static_witness_logic.ENTITIES_BY_HEX.items():
|
for entity_id, entity_object in static_witness_logic.ENTITIES_BY_HEX.items():
|
||||||
location_id = entity_object["id"]
|
location_id = entity_object["id"]
|
||||||
|
|
||||||
area = entity_object["area"]["name"]
|
area = entity_object["area"].name
|
||||||
area_to_entity_ids[area].append(entity_id)
|
area_to_entity_ids[area].append(entity_id)
|
||||||
|
|
||||||
if location_id is None:
|
if location_id is None:
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
|
|||||||
|
|
||||||
|
|
||||||
def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
|
def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
|
||||||
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
|
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.values())
|
||||||
|
|
||||||
locations_per_area = {}
|
locations_per_area = {}
|
||||||
items_per_area = {}
|
items_per_area = {}
|
||||||
@@ -472,14 +472,14 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]]
|
|||||||
for area in potential_areas:
|
for area in potential_areas:
|
||||||
regions = [
|
regions = [
|
||||||
world.get_region(region)
|
world.get_region(region)
|
||||||
for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
|
for region in area.regions
|
||||||
if region in world.player_regions.created_region_names
|
if region in world.player_regions.created_region_names
|
||||||
]
|
]
|
||||||
locations = [location for region in regions for location in region.get_locations() if not location.is_event]
|
locations = [location for region in regions for location in region.get_locations() if not location.is_event]
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
locations_per_area[area] = locations
|
locations_per_area[area.name] = locations
|
||||||
items_per_area[area] = [location.item for location in locations]
|
items_per_area[area.name] = [location.item for location in locations]
|
||||||
|
|
||||||
return locations_per_area, items_per_area
|
return locations_per_area, items_per_area
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, area_items: List[Ite
|
|||||||
hunt_panels = None
|
hunt_panels = None
|
||||||
if world.options.victory_condition == "panel_hunt" and hinted_area != "Easter Eggs":
|
if world.options.victory_condition == "panel_hunt" and hinted_area != "Easter Eggs":
|
||||||
hunt_panels = sum(
|
hunt_panels = sum(
|
||||||
static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"]["name"] == hinted_area
|
static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"].name == hinted_area
|
||||||
for hunt_entity in world.player_logic.HUNT_ENTITIES
|
for hunt_entity in world.player_logic.HUNT_ENTITIES
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -620,7 +620,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
|
|||||||
|
|
||||||
already_hinted_locations |= {
|
already_hinted_locations |= {
|
||||||
loc for loc in world.multiworld.get_reachable_locations(state, world.player)
|
loc for loc in world.multiworld.get_reachable_locations(state, world.player)
|
||||||
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
|
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"].name == "Tutorial (Inside)"
|
||||||
}
|
}
|
||||||
|
|
||||||
intended_location_hints = hint_amount - area_hints
|
intended_location_hints = hint_amount - area_hints
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from schema import And, Schema
|
from schema import And, Schema
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ from collections import defaultdict
|
|||||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
|
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
|
||||||
|
|
||||||
from .data import static_logic as static_witness_logic
|
from .data import static_logic as static_witness_logic
|
||||||
|
from .data.definition_classes import ConnectionDefinition, WitnessRule
|
||||||
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
||||||
from .data.static_logic import StaticWitnessLogicObj
|
from .data.static_logic import StaticWitnessLogicObj
|
||||||
from .data.utils import (
|
from .data.utils import (
|
||||||
WitnessRule,
|
|
||||||
get_boat,
|
get_boat,
|
||||||
get_caves_except_path_to_challenge_exclusion_list,
|
get_caves_except_path_to_challenge_exclusion_list,
|
||||||
get_complex_additional_panels,
|
get_complex_additional_panels,
|
||||||
@@ -47,7 +47,7 @@ from .data.utils import (
|
|||||||
get_vault_exclusion_list,
|
get_vault_exclusion_list,
|
||||||
logical_and_witness_rules,
|
logical_and_witness_rules,
|
||||||
logical_or_witness_rules,
|
logical_or_witness_rules,
|
||||||
parse_lambda,
|
parse_witness_rule,
|
||||||
)
|
)
|
||||||
from .entity_hunt import EntityHuntPicker
|
from .entity_hunt import EntityHuntPicker
|
||||||
|
|
||||||
@@ -97,10 +97,10 @@ class WitnessPlayerLogic:
|
|||||||
elif self.DIFFICULTY == "none":
|
elif self.DIFFICULTY == "none":
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||||
|
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, List[ConnectionDefinition]] = copy.deepcopy(
|
||||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||||
)
|
)
|
||||||
self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
self.CONNECTIONS_BY_REGION_NAME: Dict[str, List[ConnectionDefinition]] = copy.deepcopy(
|
||||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||||
)
|
)
|
||||||
self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
|
self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
|
||||||
@@ -178,7 +178,7 @@ class WitnessPlayerLogic:
|
|||||||
|
|
||||||
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
|
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
|
||||||
|
|
||||||
if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS:
|
if entity_obj["region"] is not None and entity_obj["region"].name in self.UNREACHABLE_REGIONS:
|
||||||
return frozenset()
|
return frozenset()
|
||||||
|
|
||||||
# For the requirement of an entity, we consider two things:
|
# For the requirement of an entity, we consider two things:
|
||||||
@@ -270,7 +270,7 @@ class WitnessPlayerLogic:
|
|||||||
new_items = theoretical_new_items
|
new_items = theoretical_new_items
|
||||||
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
|
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
|
||||||
new_items = frozenset(
|
new_items = frozenset(
|
||||||
frozenset(possibility | {dep_obj["region"]["name"]})
|
frozenset(possibility | {dep_obj["region"].name})
|
||||||
for possibility in new_items
|
for possibility in new_items
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -359,11 +359,11 @@ class WitnessPlayerLogic:
|
|||||||
line_split = line.split(" - ")
|
line_split = line.split(" - ")
|
||||||
|
|
||||||
requirement = {
|
requirement = {
|
||||||
"entities": parse_lambda(line_split[1]),
|
"entities": parse_witness_rule(line_split[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(line_split) > 2:
|
if len(line_split) > 2:
|
||||||
required_items = parse_lambda(line_split[2])
|
required_items = parse_witness_rule(line_split[2])
|
||||||
items_actually_in_the_game = [
|
items_actually_in_the_game = [
|
||||||
item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
|
item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
|
||||||
if item_definition.category is ItemCategory.SYMBOL
|
if item_definition.category is ItemCategory.SYMBOL
|
||||||
@@ -394,26 +394,31 @@ class WitnessPlayerLogic:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if adj_type == "New Connections":
|
if adj_type == "New Connections":
|
||||||
|
# This adjustment type does not actually reverse the connection if it could be reversed.
|
||||||
|
# If needed, this might be added later
|
||||||
line_split = line.split(" - ")
|
line_split = line.split(" - ")
|
||||||
source_region = line_split[0]
|
source_region = line_split[0]
|
||||||
target_region = line_split[1]
|
target_region = line_split[1]
|
||||||
panel_set_string = line_split[2]
|
panel_set_string = line_split[2]
|
||||||
|
|
||||||
for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
|
for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
|
||||||
if connection[0] == target_region:
|
if connection.target_region == target_region:
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
|
||||||
|
|
||||||
if panel_set_string == "TrueOneWay":
|
if panel_set_string == "TrueOneWay":
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(
|
# This means the connection can be completely replaced
|
||||||
(target_region, frozenset({frozenset(["TrueOneWay"])}))
|
only_connection = ConnectionDefinition(target_region, frozenset({frozenset(["TrueOneWay"])}))
|
||||||
)
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(only_connection)
|
||||||
else:
|
else:
|
||||||
new_lambda = logical_or_witness_rules([connection[1], parse_lambda(panel_set_string)])
|
combined_rule = logical_or_witness_rules(
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add((target_region, new_lambda))
|
[connection.traversal_rule, parse_witness_rule(panel_set_string)]
|
||||||
|
)
|
||||||
|
combined_connection = ConnectionDefinition(target_region, combined_rule)
|
||||||
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(combined_connection)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
new_conn = (target_region, parse_lambda(panel_set_string))
|
new_connection = ConnectionDefinition(target_region, parse_witness_rule(panel_set_string))
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn)
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(new_connection)
|
||||||
|
|
||||||
if adj_type == "Added Locations":
|
if adj_type == "Added Locations":
|
||||||
if "0x" in line:
|
if "0x" in line:
|
||||||
@@ -558,7 +563,7 @@ class WitnessPlayerLogic:
|
|||||||
|
|
||||||
self.AVAILABLE_EASTER_EGGS_PER_REGION = defaultdict(int)
|
self.AVAILABLE_EASTER_EGGS_PER_REGION = defaultdict(int)
|
||||||
for entity_hex in self.AVAILABLE_EASTER_EGGS:
|
for entity_hex in self.AVAILABLE_EASTER_EGGS:
|
||||||
region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"]["name"]
|
region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"].name
|
||||||
self.AVAILABLE_EASTER_EGGS_PER_REGION[region_name] += 1
|
self.AVAILABLE_EASTER_EGGS_PER_REGION[region_name] += 1
|
||||||
|
|
||||||
eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step()
|
eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step()
|
||||||
@@ -796,7 +801,7 @@ class WitnessPlayerLogic:
|
|||||||
next_region = regions_to_check.pop()
|
next_region = regions_to_check.pop()
|
||||||
|
|
||||||
for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
|
for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
|
||||||
target = region_exit[0]
|
target = region_exit.target_region
|
||||||
|
|
||||||
if target in reachable_regions:
|
if target in reachable_regions:
|
||||||
continue
|
continue
|
||||||
@@ -844,7 +849,7 @@ class WitnessPlayerLogic:
|
|||||||
|
|
||||||
# First, entities in unreachable regions are obviously themselves unreachable.
|
# First, entities in unreachable regions are obviously themselves unreachable.
|
||||||
for region in new_unreachable_regions:
|
for region in new_unreachable_regions:
|
||||||
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region]["physical_entities"]:
|
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region].physical_entities:
|
||||||
# Never disable the Victory Location.
|
# Never disable the Victory Location.
|
||||||
if entity == self.VICTORY_LOCATION:
|
if entity == self.VICTORY_LOCATION:
|
||||||
continue
|
continue
|
||||||
@@ -879,11 +884,11 @@ class WitnessPlayerLogic:
|
|||||||
if not new_unreachable_regions and not newly_discovered_disabled_entities:
|
if not new_unreachable_regions and not newly_discovered_disabled_entities:
|
||||||
return
|
return
|
||||||
|
|
||||||
def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule:
|
def reduce_connection_requirement(self, connection: ConnectionDefinition) -> ConnectionDefinition:
|
||||||
all_possibilities = []
|
all_possibilities = []
|
||||||
|
|
||||||
# Check each traversal option individually
|
# Check each traversal option individually
|
||||||
for option in connection[1]:
|
for option in connection.traversal_rule:
|
||||||
individual_entity_requirements: List[WitnessRule] = []
|
individual_entity_requirements: List[WitnessRule] = []
|
||||||
for entity in option:
|
for entity in option:
|
||||||
# If a connection requires solving a disabled entity, it is not valid.
|
# If a connection requires solving a disabled entity, it is not valid.
|
||||||
@@ -901,7 +906,7 @@ class WitnessPlayerLogic:
|
|||||||
entity_req = self.get_entity_requirement(entity)
|
entity_req = self.get_entity_requirement(entity)
|
||||||
|
|
||||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
|
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
|
||||||
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
|
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"].name
|
||||||
entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
|
entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
|
||||||
|
|
||||||
individual_entity_requirements.append(entity_req)
|
individual_entity_requirements.append(entity_req)
|
||||||
@@ -909,7 +914,7 @@ class WitnessPlayerLogic:
|
|||||||
# Merge all possible requirements into one DNF condition.
|
# Merge all possible requirements into one DNF condition.
|
||||||
all_possibilities.append(logical_and_witness_rules(individual_entity_requirements))
|
all_possibilities.append(logical_and_witness_rules(individual_entity_requirements))
|
||||||
|
|
||||||
return logical_or_witness_rules(all_possibilities)
|
return ConnectionDefinition(connection.target_region, logical_or_witness_rules(all_possibilities))
|
||||||
|
|
||||||
def make_dependency_reduced_checklist(self) -> None:
|
def make_dependency_reduced_checklist(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -942,14 +947,14 @@ class WitnessPlayerLogic:
|
|||||||
|
|
||||||
# Make independent region connection requirements based on the entities they require
|
# Make independent region connection requirements based on the entities they require
|
||||||
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
|
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
|
||||||
new_connections = set()
|
new_connections = []
|
||||||
|
|
||||||
for connection in connections:
|
for connection in connections:
|
||||||
overall_requirement = self.reduce_connection_requirement(connection)
|
reduced_connection = self.reduce_connection_requirement(connection)
|
||||||
|
|
||||||
# If there is a way to use this connection, add it.
|
# If there is a way to use this connection, add it.
|
||||||
if overall_requirement:
|
if reduced_connection.can_be_traversed:
|
||||||
new_connections.add((connection[0], overall_requirement))
|
new_connections.append(reduced_connection)
|
||||||
|
|
||||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ from BaseClasses import Entrance, Region
|
|||||||
from worlds.generic.Rules import CollectionRule
|
from worlds.generic.Rules import CollectionRule
|
||||||
|
|
||||||
from .data import static_logic as static_witness_logic
|
from .data import static_logic as static_witness_logic
|
||||||
|
from .data.definition_classes import WitnessRule
|
||||||
from .data.static_logic import StaticWitnessLogicObj
|
from .data.static_logic import StaticWitnessLogicObj
|
||||||
from .data.utils import WitnessRule, optimize_witness_rule
|
from .data.utils import optimize_witness_rule
|
||||||
from .locations import WitnessPlayerLocations
|
from .locations import WitnessPlayerLocations
|
||||||
from .player_logic import WitnessPlayerLogic
|
from .player_logic import WitnessPlayerLogic
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ class WitnessPlayerRegions:
|
|||||||
if k not in player_logic.UNREACHABLE_REGIONS
|
if k not in player_logic.UNREACHABLE_REGIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
event_locations_per_region = defaultdict(dict)
|
event_locations_per_region: Dict[str, Dict[str, int]] = defaultdict(dict)
|
||||||
|
|
||||||
for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items():
|
for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items():
|
||||||
entity_or_region = event_item_and_entity[1]
|
entity_or_region = event_item_and_entity[1]
|
||||||
@@ -126,13 +127,13 @@ class WitnessPlayerRegions:
|
|||||||
if region is None:
|
if region is None:
|
||||||
region_name = "Entry"
|
region_name = "Entry"
|
||||||
else:
|
else:
|
||||||
region_name = region["name"]
|
region_name = region.name
|
||||||
order = self.reference_logic.ENTITIES_BY_HEX[entity_or_region]["order"]
|
order = self.reference_logic.ENTITIES_BY_HEX[entity_or_region]["order"]
|
||||||
event_locations_per_region[region_name][event_location] = order
|
event_locations_per_region[region_name][event_location] = order
|
||||||
|
|
||||||
for region_name, region in regions_to_create.items():
|
for region_name, region in regions_to_create.items():
|
||||||
location_entities_for_this_region = [
|
location_entities_for_this_region = [
|
||||||
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region["entities"]
|
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region.logical_entities
|
||||||
]
|
]
|
||||||
locations_for_this_region = {
|
locations_for_this_region = {
|
||||||
entity["checkName"]: entity["order"] for entity in location_entities_for_this_region
|
entity["checkName"]: entity["order"] for entity in location_entities_for_this_region
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from BaseClasses import CollectionState
|
|||||||
from worlds.generic.Rules import CollectionRule, set_rule
|
from worlds.generic.Rules import CollectionRule, set_rule
|
||||||
|
|
||||||
from .data import static_logic as static_witness_logic
|
from .data import static_logic as static_witness_logic
|
||||||
from .data.utils import WitnessRule
|
from .data.definition_classes import WitnessRule
|
||||||
from .player_logic import WitnessPlayerLogic
|
from .player_logic import WitnessPlayerLogic
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -516,6 +516,6 @@ async def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def launch() -> None:
|
def launch() -> None:
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ def main() -> None:
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(_main())
|
asyncio.run(_main())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user