Compare commits

...

32 Commits

Author SHA1 Message Date
NewSoupVi
9a351be44b The Witness: Rename "Panel Hunt Settings" to "Panel Hunt Options"
Who let me get away with this lmao
2024-11-26 21:17:24 +01:00
Ziktofel
0dade05133 SC2: Fix wrongly classified location type (#4249) 2024-11-26 00:35:24 +01:00
Exempt-Medic
fcaba14b62 Zillion: Add display_name to ZillionSkill #4241 2024-11-25 19:27:31 +01:00
Exempt-Medic
6073d5e37e Lufia2: Fix Nondeterministic Behavior #4243 2024-11-25 19:26:44 +01:00
Exempt-Medic
41a7d7eeee HK: Fix Nondeterministic Behavior #4244 2024-11-25 19:26:21 +01:00
Exempt-Medic
d3a3c29bc9 Landstalker: Fix Nondeterministic Behavior #4245 2024-11-25 19:25:55 +01:00
wildham
0ad5b0ade8 [FFMQ] Fix all checks sending on hard reset + stronger read validation check (#4242)
* Fix all checks sending on hard reset

* stronger validation

* Fix typo

* remove extraneous else

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* fix style

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-11-25 19:25:29 +01:00
Exempt-Medic
e6e31a27e6 SC2: Fix Nondeterministic Behavior (#4246)
* Add < for sorting

* Sorting for determinism

* id instead of value
2024-11-25 19:25:00 +01:00
Scipio Wright
a650e90b57 TUNIC: Add clarifying comment to item links handling #4233 2024-11-24 18:43:28 +01:00
gaithern
36f17111bf Kingdom Hearts: Minor Logic Fixes (#4236)
* Update Rules.py

* Update worlds/kh1/Rules.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/kh1/Rules.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-11-24 18:42:21 +01:00
Jarno
03b90cf39b Timespinner: Re-added missing enmemy rando option #4235 2024-11-24 15:57:39 +01:00
Scipio Wright
5729b78504 TUNIC: Fix it so item linked locations are correct in slot data (#4105)
* Fix it so item linked locations are correct in slot data

* List -> Set

* Cache the locations instead

* Just loop the multiworld once

* Move it all to fill slot data and pretend we're doing a stage

* Move groups up so it doesn't loop over the multiworld locations if no item links are present

* Update worlds/tunic/__init__.py

Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>

---------

Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>
2024-11-23 01:42:44 +01:00
Mysteryem
ba50c947ba AHiT: Fix reconnecting rift access regions for starting and plando acts (#4200)
Reconnecting an act in a telescope to a time rift removes the entrances
to the time rift from its access regions because it will be accessible
from the telescope instead.

By doing so early on, as a starting act with insanity act randomizer or
as a plando-ed act, this can happen before the time rift itself has been
reconnected to an act or other time rift. In which case, when later
attempting to connect that time rift to an act or other time rift, the
entrances from the rift access regions will no longer exist, so must be
re-created. The original code was mistakenly re-creating the entrances
from the time rift being reconnected, instead of from the rift access
regions.
2024-11-23 00:13:57 +01:00
digiholic
2424b79626 OSRS: Fixes to Logic errors related to Max Skill Level determining when Regions are accessible (#4188)
* Removes explicit indirect conditions

* Changes special rules function add rule instead of setting, and call it unconditionally

* Fixes issues in rule generation that have been around but unused the whole time

* Finally moves rules out into a separate file. Fixes level-related logic

* Removes redundant max skill level checks on canoes, since they're in the skill training rules now

* For some reason, canoe logic assumed you could always walk from lumbridge to south varrock without farms. This has been fixed

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Quests now respect skill limits and can be excluded. Tasks that take multiple skills how actually check all skills

* Adds alternative route for cooking that doesn't require fishing

* Remove debug code

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-11-22 16:33:27 +01:00
Mysteryem
d4b1351c99 Aquaria: Remove BaseException handling from create_item (#4218)
* Aquaria: Remove BaseException handling from create_item

Catching `BaseException` without re-raising the exception should almost
never be done because `BaseException` includes exit exceptions, such as
`SystemExit` and `KeyboardInterrupt`.

Ideally, the caught exception types should be as narrow as possible to
not mask bugs from catching unexpected exceptions. Having narrow
exception types can also help indicate to other developers what
exceptions are expected to be raisable by the code within the `try`
clause.

Similarly, the `try` clause should ideally contain the minimum code
necessary, to avoid masking bugs in the case that code within the `try`
clause that is not expected to raise an exception does so.

In this case, the only expected exception that can occur appears to be
`item_table[name]` that can raise a `KeyError` when `create_item()` is
passed an unexpected `name` argument. So this patch moves the other code
out of the `try` clause and changes the caught exception types to only
`KeyError`.

* Remove try-except

The KeyError that would be raised will be propagated as-is rather than
raising a new exception in its place.

* Remove extra newline

The original code did not have this newline, so it has been removed.
2024-11-21 20:43:37 +01:00
qwint
859ae87ec9 Launcher: ports the _stop fix in the Launcher kivy App to handle_url Popup App (#4213)
* fixes url launched popup so it can close cleanly after spawning another kivy app like text client

* whoops
2024-11-21 17:43:01 +01:00
Doug Hoskisson
124ce13da7 Core: improve error message for missing "game" entry in yaml (#4185) 2024-11-20 09:45:41 +01:00
qwint
48ea274655 MultiServer: persist hints even if previously found (#4214)
* change to persist all hints to ctx.hints regardless of found status

* remove if not found entirely as it seems like it was added to not double charge hint points
9842399d8b
2024-11-19 21:16:10 +01:00
Aaron Wagener
85a713771b Tests: have option preset validation test do full validation (#4208)
* Tests: have option preset validation test do full validation

* sum on an IntFlag is a thing apparently
2024-11-18 18:09:27 +01:00
black-sliver
3ae8992fb6 Clients: fix high CPU usage when launched via MultiProcessing (#4209)
* Core: make Utils.stream_input not consume all CPU for non-blocking streams

* Clients: ignore MultiProcessing pipe as input console
2024-11-18 15:59:17 +01:00
Scipio Wright
01c6037562 TUNIC: Fix a few missing tricks in logic (#4132)
* Add missing connection to the furnace entry by west garden

* Add missing connection to the furnace entry by west garden

* Add missing hard ls for ruined passage door

* Allow shield for LS

* Split PR into two

* Split PR into two

* Split PR into two

* Add dark tomb ice grapple through the wall
2024-11-18 14:39:58 +01:00
agilbert1412
4b80b786e2 Stardew Valley: Removed Walnutsanity and Filler buffs from the all random preset (#4206) 2024-11-18 08:45:04 +01:00
Silvris
bd5c8ec172 MM2: minor bugfixes (#4190)
* move special cases to be outside strict

* Update text.py

* fix wily machine edge case, incorrect weapons, and time stopper failsafe

* bump world version

* weakness checking is inclusive

* Update __init__.py

* add air shooter to edge case validation
2024-11-18 02:22:25 +01:00
t3hf1gm3nt
baf291d7a2 TLOZ: Assorted Logic Fixes (#4203)
* TLOZ: Assorded Logic Fixes

- Add needing arrows for Pols Voice rule. Not super necessary at the moment since wooden arrows are always accessible in one of the opening shops, but future proofing for future plans

- Create Gohma Locations and make sure all Gohma blocked locations have the required rule (was missing at least one location before)

- Remove the rule requiring Bow for all locations of level 8 (not sure why that was there, it's theoretically redundant now that Gohma and Pols Voice are properly marked)

- Make sure Digdogger locations properly require Recorder, and clean up redundant Level 7 rules as level 7 currently requires Recorder to access the entrance

* Update worlds/tloz/Rules.py

forgor that has_any exists

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Remove world = multiworld

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-11-18 02:19:26 +01:00
NewSoupVi
9c102da901 The Witness: Allow setting the puzzle randomization seed yourself (#4196)
* Allow setting the puzzle randomization seed yourself

* longer tooltip

* Oh

* Also actually have the correct values that the client will accept (lol, thanks Medic)

* Update worlds/witness/options.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-11-18 02:16:14 +01:00
Louis M
75e18e3cc9 Aquaria: Fixing no progression bug (#4199) 2024-11-17 16:59:50 +01:00
CaitSith2
a3d6036939 Factorio: energy link bridge improvements (#4182)
* improve energy link performance on large surfaces

* Add Energy link bridge storage table to initialization.

* Fix event based energy link for Factorio 2.0

* Adjust energy link bridge for quality.
2024-11-17 16:58:14 +01:00
Mysteryem
7eb12174b7 Core: Fix empty rule comparisons with subclasses (#4201)
If a world uses a `Location` or `Entrance` subclass that overrides the
`item_rule`/`access_rule` class attribute, then
`spot.__class__.item_rule`/`spot.__class__.access_rule` will get the
overridden rule, which may not be an empty rule.

Uses of `spot.__class__` have been replaced with getting the class
attribute rule belonging to the `Location` or `Entrance` class.
2024-11-17 16:55:42 +01:00
NewSoupVi
73146ef30c Tests: Use Option.from_any instead of Option() in test_pickle_dumps, which is currently preventing Range options from using default: "random" #4197 2024-11-17 01:52:49 +01:00
Fabian Dill
66314de965 Subnautica: compose DeathLink custom text instead of overwriting (#4172)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-11-17 00:55:18 +01:00
Zach "Phar" Parks
5141f36e95 WebHost: Fix 500 server errors for hints involving ItemLink slots on tracker pages (#4198)
* Also makes adjustments to the style for these slots by italicizing its names (including multi-tracker).
* Player-specific trackers do not link to ItemLink player trackers (they do not exist).
* Fixes a bug on Factorio multi-tracker when item links exist.
2024-11-16 16:16:09 +00:00
black-sliver
9ba613277e Launcher: change import order to fix ModuleUpdate (#4194) 2024-11-16 03:00:34 +01:00
45 changed files with 779 additions and 642 deletions

View File

@@ -710,6 +710,11 @@ class CommonContext:
def run_cli(self): def run_cli(self):
if sys.stdin: if sys.stdin:
if sys.stdin.fileno() != 0:
from multiprocessing import parent_process
if parent_process():
return # ignore MultiProcessing pipe
# steam overlay breaks when starting console_loop # steam overlay breaks when starting console_loop
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")

View File

@@ -453,6 +453,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights) ret.game = get_choice("game", weights)
if not isinstance(ret.game, str):
if ret.game is None:
raise Exception('"game" not specified')
raise Exception(f"Invalid game: {ret.game}")
if ret.game not in AutoWorldRegister.world_types: if ret.game not in AutoWorldRegister.world_types:
from worlds import failed_world_loads from worlds import failed_world_loads
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]

View File

@@ -22,16 +22,15 @@ from os.path import isfile
from shutil import which from shutil import which
from typing import Callable, Optional, Sequence, Tuple, Union from typing import Callable, Optional, Sequence, Tuple, Union
import Utils
import settings
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
if __name__ == "__main__": if __name__ == "__main__":
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ import settings
is_windows, is_macos, is_linux import Utils
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
user_path)
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
def open_host_yaml(): def open_host_yaml():
@@ -182,6 +181,11 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
App.get_running_app().stop() App.get_running_app().stop()
Window.close() Window.close()
def _stop(self, *largs):
# see run_gui Launcher _stop comment for details
self.root_window.close()
super()._stop(*largs)
Popup().run() Popup().run()

View File

@@ -727,15 +727,15 @@ class Context:
if not hint.local and data not in concerns[hint.finding_player]: if not hint.local and data not in concerns[hint.finding_player]:
concerns[hint.finding_player].append(data) concerns[hint.finding_player].append(data)
# remember hints in all cases # remember hints in all cases
if not hint.found:
# since hints are bidirectional, finding player and receiving player, # since hints are bidirectional, finding player and receiving player,
# we can check once if hint already exists # we can check once if hint already exists
if hint not in self.hints[team, hint.finding_player]: if hint not in self.hints[team, hint.finding_player]:
self.hints[team, hint.finding_player].add(hint) self.hints[team, hint.finding_player].add(hint)
new_hint_events.add(hint.finding_player) new_hint_events.add(hint.finding_player)
for player in self.slot_set(hint.receiving_player): for player in self.slot_set(hint.receiving_player):
self.hints[team, player].add(hint) self.hints[team, player].add(hint)
new_hint_events.add(player) new_hint_events.add(player)
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
for slot in new_hint_events: for slot in new_hint_events:

View File

@@ -18,6 +18,7 @@ import warnings
from argparse import Namespace from argparse import Namespace
from settings import Settings, get_settings from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard from typing_extensions import TypeGuard
from yaml import load, load_all, dump from yaml import load, load_all, dump
@@ -568,6 +569,8 @@ def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
else: else:
if text: if text:
queue.put_nowait(text) queue.put_nowait(text)
else:
sleep(0.01) # non-blocking stream
from threading import Thread from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)

View File

@@ -98,6 +98,8 @@
<td> <td>
{% if hint.finding_player == player %} {% if hint.finding_player == player %}
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b> <b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
{% elif get_slot_info(team, hint.finding_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
{% else %} {% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}"> <a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
{{ player_names_with_alias[(team, hint.finding_player)] }} {{ player_names_with_alias[(team, hint.finding_player)] }}
@@ -107,6 +109,8 @@
<td> <td>
{% if hint.receiving_player == player %} {% if hint.receiving_player == player %}
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b> <b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
{% elif get_slot_info(team, hint.receiving_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
{% else %} {% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}"> <a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
{{ player_names_with_alias[(team, hint.receiving_player)] }} {{ player_names_with_alias[(team, hint.receiving_player)] }}

View File

@@ -21,8 +21,20 @@
) )
-%} -%}
<tr> <tr>
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td> <td>
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td> {% if get_slot_info(team, hint.finding_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
{% else %}
{{ player_names_with_alias[(team, hint.finding_player)] }}
{% endif %}
</td>
<td>
{% if get_slot_info(team, hint.receiving_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
{% else %}
{{ player_names_with_alias[(team, hint.receiving_player)] }}
{% endif %}
</td>
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td> <td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td> <td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
<td>{{ games[(team, hint.finding_player)] }}</td> <td>{{ games[(team, hint.finding_player)] }}</td>

View File

@@ -423,6 +423,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) ->
template_name_or_list="genericTracker.html", template_name_or_list="genericTracker.html",
game_specific_tracker=game in _player_trackers, game_specific_tracker=game in _player_trackers,
room=tracker_data.room, room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
team=team, team=team,
player=player, player=player,
player_name=tracker_data.get_room_long_player_names()[team, player], player_name=tracker_data.get_room_long_player_names()[team, player],
@@ -446,6 +447,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
enabled_trackers=enabled_trackers, enabled_trackers=enabled_trackers,
current_tracker="Generic", current_tracker="Generic",
room=tracker_data.room, room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
all_slots=tracker_data.get_all_slots(), all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(), room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(), locations=tracker_data.get_room_locations(),
@@ -497,7 +499,7 @@ if "Factorio" in network_data_package["games"]:
(team, player): collections.Counter({ (team, player): collections.Counter({
tracker_data.item_id_to_name["Factorio"][item_id]: count tracker_data.item_id_to_name["Factorio"][item_id]: count
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items() for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
}) for team, players in tracker_data.get_all_slots().items() for player in players }) for team, players in tracker_data.get_all_players().items() for player in players
if tracker_data.get_player_game(team, player) == "Factorio" if tracker_data.get_player_game(team, player) == "Factorio"
} }
@@ -506,6 +508,7 @@ if "Factorio" in network_data_package["games"]:
enabled_trackers=enabled_trackers, enabled_trackers=enabled_trackers,
current_tracker="Factorio", current_tracker="Factorio",
room=tracker_data.room, room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
all_slots=tracker_data.get_all_slots(), all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(), room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(), locations=tracker_data.get_room_locations(),
@@ -638,6 +641,7 @@ if "A Link to the Past" in network_data_package["games"]:
enabled_trackers=enabled_trackers, enabled_trackers=enabled_trackers,
current_tracker="A Link to the Past", current_tracker="A Link to the Past",
room=tracker_data.room, room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
all_slots=tracker_data.get_all_slots(), all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(), room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(), locations=tracker_data.get_room_locations(),

View File

@@ -78,4 +78,4 @@ class TestOptions(unittest.TestCase):
if not world_type.hidden: if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items(): for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key): with self.subTest(game=gamename, option=option_key):
pickle.dumps(option(option.default)) pickle.dumps(option.from_any(option.default))

View File

@@ -1,5 +1,6 @@
import unittest import unittest
from BaseClasses import PlandoOptions
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
@@ -14,6 +15,10 @@ class TestOptionPresets(unittest.TestCase):
with self.subTest(game=game_name, preset=preset_name, option=option_name): with self.subTest(game=game_name, preset=preset_name, option=option_name):
try: try:
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
# some options may need verification to ensure the provided option is actually valid
# pass in all plando options in case a preset wants to require certain plando options
# for some reason
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
supported_types = [NumericOption, OptionSet, OptionList, ItemDict] supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
if not any([issubclass(option.__class__, t) for t in supported_types]): if not any([issubclass(option.__class__, t) for t in supported_types]):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "

View File

@@ -740,17 +740,20 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region):
i = 1 for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1):
while i <= len(rift_access_regions[time_rift.name]): # Matches the naming convention and iteration order in `create_rift_connections()`.
name = f"{time_rift.name} Portal - Entrance {i}" name = f"{time_rift.name} Portal - Entrance {i}"
entrance: Entrance entrance: Entrance
try: try:
entrance = world.multiworld.get_entrance(name, world.player) entrance = world.get_entrance(name)
# Reconnect the rift access region to the new exit region.
reconnect_regions(entrance, entrance.parent_region, exit_region) reconnect_regions(entrance, entrance.parent_region, exit_region)
except KeyError: except KeyError:
time_rift.connect(exit_region, name) # The original entrance to the time rift has been deleted by already reconnecting a telescope act to the
# time rift, so create a new entrance from the original rift access region to the new exit region.
i += 1 # Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but
# starting acts/rifts and act-plando can reconnect acts to time rifts before this happens.
world.get_region(access_region).connect(exit_region, name)
def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]: def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]:

View File

@@ -1152,79 +1152,79 @@ class AquariaRegions:
def __no_progression_hard_or_hidden_location(self) -> None: def __no_progression_hard_or_hidden_location(self) -> None:
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Mithalas boss area, beating Mithalan God", self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.multiworld.get_location("Sun Temple boss area, beating Sun God",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.multiworld.get_location("Sunken City, bulb on top of the boss area",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Home Water, Nautilus Egg", self.multiworld.get_location("Home Water, Nautilus Egg",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.multiworld.get_location("Mithalas City Castle, beating the Priests",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Mermog cave, Piranha Egg", self.multiworld.get_location("Mermog cave, Piranha Egg",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.multiworld.get_location("Octopus Cave, Dumbo Egg",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Sun Worm path, first cliff bulb", self.multiworld.get_location("Sun Worm path, first cliff bulb",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Sun Worm path, second cliff bulb", self.multiworld.get_location("Sun Worm path, second cliff bulb",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Bubble Cave, Verse Egg", self.multiworld.get_location("Bubble Cave, Verse Egg",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Sun Temple, Sun Key", self.multiworld.get_location("Sun Temple, Sun Key",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("The Body bottom area, Mutant Costume", self.multiworld.get_location("The Body bottom area, Mutant Costume",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
self.player).item_rule = \ self.player).item_rule = \
lambda item: item.classification != ItemClassification.progression lambda item: not item.advancement
def adjusting_rules(self, options: AquariaOptions) -> None: def adjusting_rules(self, options: AquariaOptions) -> None:
""" """

View File

@@ -117,16 +117,13 @@ class AquariaWorld(World):
Create an AquariaItem using 'name' as item name. Create an AquariaItem using 'name' as item name.
""" """
result: AquariaItem result: AquariaItem
try: data = item_table[name]
data = item_table[name] classification: ItemClassification = ItemClassification.useful
classification: ItemClassification = ItemClassification.useful if data.type == ItemType.JUNK:
if data.type == ItemType.JUNK: classification = ItemClassification.filler
classification = ItemClassification.filler elif data.type == ItemType.PROGRESSION:
elif data.type == ItemType.PROGRESSION: classification = ItemClassification.progression
classification = ItemClassification.progression result = AquariaItem(name, classification, data.id, self.player)
result = AquariaItem(name, classification, data.id, self.player)
except BaseException:
raise Exception('The item ' + name + ' is not valid.')
return result return result

View File

@@ -49,7 +49,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
for location in self.unfillable_locations: for location in self.unfillable_locations:
for item_name in self.world.item_names: for item_name in self.world.item_names:
item = self.get_item_by_name(item_name) item = self.get_item_by_name(item_name)
if item.classification == ItemClassification.progression: if item.advancement:
self.assertFalse( self.assertFalse(
self.world.get_location(location).can_fill(self.multiworld.state, item, False), self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" can be filled with \"" + item_name + "\"") "The location \"" + location + "\" can be filled with \"" + item_name + "\"")

View File

@@ -105,8 +105,8 @@ function on_player_changed_position(event)
end end
local target_direction = exit_table[outbound_direction] local target_direction = exit_table[outbound_direction]
local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
(CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
target_position = character.surface.find_non_colliding_position(character.prototype.name, target_position = character.surface.find_non_colliding_position(character.prototype.name,
target_position, 32, 0.5) target_position, 32, 0.5)
if target_position ~= nil then if target_position ~= nil then
@@ -134,40 +134,96 @@ end
script.on_event(defines.events.on_player_changed_position, on_player_changed_position) script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
{% endif %} {% endif %}
function count_energy_bridges()
local count = 0
for i, bridge in pairs(storage.energy_link_bridges) do
if validate_energy_link_bridge(i, bridge) then
count = count + 1 + (bridge.quality.level * 0.3)
end
end
return count
end
function get_energy_increment(bridge)
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
end
function on_check_energy_link(event) function on_check_energy_link(event)
--- assuming 1 MJ increment and 5MJ battery: --- assuming 1 MJ increment and 5MJ battery:
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
if event.tick % 60 == 30 then if event.tick % 60 == 30 then
local surface = game.get_surface(1)
local force = "player" local force = "player"
local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force}) local bridges = storage.energy_link_bridges
local bridgecount = table_size(bridges) local bridgecount = count_energy_bridges()
storage.forcedata[force].energy_bridges = bridgecount storage.forcedata[force].energy_bridges = bridgecount
if storage.forcedata[force].energy == nil then if storage.forcedata[force].energy == nil then
storage.forcedata[force].energy = 0 storage.forcedata[force].energy = 0
end end
if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then
for i, bridge in ipairs(bridges) do for i, bridge in pairs(bridges) do
if bridge.energy > ENERGY_INCREMENT*3 then if validate_energy_link_bridge(i, bridge) then
storage.forcedata[force].energy = storage.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY) energy_increment = get_energy_increment(bridge)
bridge.energy = bridge.energy - ENERGY_INCREMENT if bridge.energy > energy_increment*3 then
storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY)
bridge.energy = bridge.energy - energy_increment
end
end end
end end
end end
for i, bridge in ipairs(bridges) do for i, bridge in pairs(bridges) do
if storage.forcedata[force].energy < ENERGY_INCREMENT then if validate_energy_link_bridge(i, bridge) then
break energy_increment = get_energy_increment(bridge)
end if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then
if bridge.energy < ENERGY_INCREMENT*2 and storage.forcedata[force].energy > ENERGY_INCREMENT then break
storage.forcedata[force].energy = storage.forcedata[force].energy - ENERGY_INCREMENT end
bridge.energy = bridge.energy + ENERGY_INCREMENT if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then
storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment
bridge.energy = bridge.energy + energy_increment
end
end end
end end
end end
end end
function string_starts_with(str, start)
return str:sub(1, #start) == start
end
function validate_energy_link_bridge(unit_number, entity)
if not entity then
if storage.energy_link_bridges[unit_number] == nil then return false end
storage.energy_link_bridges[unit_number] = nil
return false
end
if not entity.valid then
if storage.energy_link_bridges[unit_number] == nil then return false end
storage.energy_link_bridges[unit_number] = nil
return false
end
return true
end
function on_energy_bridge_constructed(entity)
if entity and entity.valid then
if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
storage.energy_link_bridges[entity.unit_number] = entity
end
end
end
function on_energy_bridge_removed(entity)
if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
if storage.energy_link_bridges[entity.unit_number] == nil then return end
storage.energy_link_bridges[entity.unit_number] = nil
end
end
if (ENERGY_INCREMENT) then if (ENERGY_INCREMENT) then
script.on_event(defines.events.on_tick, on_check_energy_link) script.on_event(defines.events.on_tick, on_check_energy_link)
script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end)
script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end)
script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
end end
{% if not imported_blueprints -%} {% if not imported_blueprints -%}
@@ -410,6 +466,7 @@ script.on_init(function()
{% if not imported_blueprints %}set_permissions(){% endif %} {% if not imported_blueprints %}set_permissions(){% endif %}
storage.forcedata = {} storage.forcedata = {}
storage.playerdata = {} storage.playerdata = {}
storage.energy_link_bridges = {}
-- Fire dummy events for all currently existing forces. -- Fire dummy events for all currently existing forces.
local e = {} local e = {}
for name, _ in pairs(game.forces) do for name, _ in pairs(game.forces) do

View File

@@ -47,6 +47,17 @@ def get_flag(data, flag):
bit = int(0x80 / (2 ** (flag % 8))) bit = int(0x80 / (2 ** (flag % 8)))
return (data[byte] & bit) > 0 return (data[byte] & bit) > 0
def validate_read_state(data1, data2):
validation_array = bytes([0x01, 0x46, 0x46, 0x4D, 0x51, 0x52])
if data1 is None or data2 is None:
return False
for i in range(6):
if data1[i] != validation_array[i] or data2[i] != validation_array[i]:
return False;
return True
class FFMQClient(SNIClient): class FFMQClient(SNIClient):
game = "Final Fantasy Mystic Quest" game = "Final Fantasy Mystic Quest"
@@ -67,11 +78,11 @@ class FFMQClient(SNIClient):
async def game_watcher(self, ctx): async def game_watcher(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
check_1 = await snes_read(ctx, 0xF53749, 1) check_1 = await snes_read(ctx, 0xF53749, 6)
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
check_2 = await snes_read(ctx, 0xF53749, 1) check_2 = await snes_read(ctx, 0xF53749, 6)
if check_1 != b'\x01' or check_2 != b'\x01': if not validate_read_state(check_1, check_2):
return return
def get_range(data_range): def get_range(data_range):

View File

@@ -69,7 +69,7 @@ def locality_rules(multiworld: MultiWorld):
if (location.player, location.item_rule) in func_cache: if (location.player, location.item_rule) in func_cache:
location.item_rule = func_cache[location.player, location.item_rule] location.item_rule = func_cache[location.player, location.item_rule]
# empty rule that just returns True, overwrite # empty rule that just returns True, overwrite
elif location.item_rule is location.__class__.item_rule: elif location.item_rule is Location.item_rule:
func_cache[location.player, location.item_rule] = location.item_rule = \ func_cache[location.player, location.item_rule] = location.item_rule = \
lambda i, sending_blockers = forbid_data[location.player], \ lambda i, sending_blockers = forbid_data[location.player], \
old_rule = location.item_rule: \ old_rule = location.item_rule: \
@@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"):
old_rule = spot.access_rule old_rule = spot.access_rule
# empty rule, replace instead of add # empty rule, replace instead of add
if old_rule is spot.__class__.access_rule: if old_rule is Location.access_rule or old_rule is Entrance.access_rule:
spot.access_rule = rule if combine == "and" else old_rule spot.access_rule = rule if combine == "and" else old_rule
else: else:
if combine == "and": if combine == "and":
@@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
def forbid_item(location: "BaseClasses.Location", item: str, player: int): def forbid_item(location: "BaseClasses.Location", item: str, player: int):
old_rule = location.item_rule old_rule = location.item_rule
# empty rule # empty rule
if old_rule is location.__class__.item_rule: if old_rule is Location.item_rule:
location.item_rule = lambda i: i.name != item or i.player != player location.item_rule = lambda i: i.name != item or i.player != player
else: else:
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
@@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"):
old_rule = location.item_rule old_rule = location.item_rule
# empty rule, replace instead of add # empty rule, replace instead of add
if old_rule is location.__class__.item_rule: if old_rule is Location.item_rule:
location.item_rule = rule if combine == "and" else old_rule location.item_rule = rule if combine == "and" else old_rule
else: else:
if combine == "and": if combine == "and":

View File

@@ -231,7 +231,7 @@ class HKWorld(World):
all_event_names.update(set(godhome_event_names)) all_event_names.update(set(godhome_event_names))
# Link regions # Link regions
for event_name in all_event_names: for event_name in sorted(all_event_names):
#if event_name in wp_exclusions: #if event_name in wp_exclusions:
# continue # continue
loc = HKLocation(self.player, event_name, None, menu_region) loc = HKLocation(self.player, event_name, None, menu_region)

View File

@@ -235,6 +235,11 @@ def set_rules(kh1world):
lambda state: ( lambda state: (
state.has("Progressive Glide", player) state.has("Progressive Glide", player)
or or
(
state.has("High Jump", player, 2)
and state.has("Footprints", player)
)
or
( (
options.advanced_logic options.advanced_logic
and state.has_all({ and state.has_all({
@@ -246,6 +251,11 @@ def set_rules(kh1world):
lambda state: ( lambda state: (
state.has("Progressive Glide", player) state.has("Progressive Glide", player)
or or
(
state.has("High Jump", player, 2)
and state.has("Footprints", player)
)
or
( (
options.advanced_logic options.advanced_logic
and state.has_all({ and state.has_all({
@@ -258,7 +268,6 @@ def set_rules(kh1world):
state.has("Footprints", player) state.has("Footprints", player)
or (options.advanced_logic and state.has("Progressive Glide", player)) or (options.advanced_logic and state.has("Progressive Glide", player))
or state.has("High Jump", player, 2)
)) ))
add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"), add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"),
lambda state: ( lambda state: (
@@ -376,7 +385,7 @@ def set_rules(kh1world):
lambda state: state.has("White Trinity", player)) lambda state: state.has("White Trinity", player))
add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"), add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"),
lambda state: ( lambda state: (
state.has("High Jump", player) state.has_all(("High Jump", "Progressive Glide"), player)
or (options.advanced_logic and state.has("Combo Master", player)) or (options.advanced_logic and state.has("Combo Master", player))
)) ))
add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"), add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"),
@@ -386,7 +395,7 @@ def set_rules(kh1world):
)) ))
add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"), add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"),
lambda state: ( lambda state: (
state.has("High Jump", player) state.has_all(("High Jump", "Progressive Glide"), player)
or (options.advanced_logic and state.has("Combo Master", player)) or (options.advanced_logic and state.has("Combo Master", player))
)) ))
add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"), add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"),
@@ -595,6 +604,7 @@ def set_rules(kh1world):
lambda state: ( lambda state: (
state.has("Green Trinity", player) state.has("Green Trinity", player)
and has_all_magic_lvx(state, player, 2) and has_all_magic_lvx(state, player, 2)
and has_defensive_tools(state, player)
)) ))
add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"), add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"),
lambda state: ( lambda state: (
@@ -710,8 +720,7 @@ def set_rules(kh1world):
lambda state: state.has("White Trinity", player)) lambda state: state.has("White Trinity", player))
add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"), add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"),
lambda state: ( lambda state: (
state.has("High Jump", player) state.has("Progressive Glide", player)
or state.has("Progressive Glide", player)
)) ))
add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"), add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"),
lambda state: ( lambda state: (
@@ -1441,10 +1450,11 @@ def set_rules(kh1world):
has_emblems(state, player, options.keyblades_unlock_chests) has_emblems(state, player, options.keyblades_unlock_chests)
and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests)
and has_defensive_tools(state, player) and has_defensive_tools(state, player)
and state.has("Progressive Blizzard", player, 3)
)) ))
add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"), add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"),
lambda state: ( lambda state: (
has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3)
)) ))
if options.super_bosses or options.goal.current_key == "sephiroth": if options.super_bosses or options.goal.current_key == "sephiroth":
add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"), add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"),

View File

@@ -34,7 +34,7 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n
for data in WORLD_PATHS_JSON: for data in WORLD_PATHS_JSON:
if "requiredNodes" in data: if "requiredNodes" in data:
regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]])
regions_with_entrance_checks = list(set(regions_with_entrance_checks)) regions_with_entrance_checks = sorted(set(regions_with_entrance_checks))
for region_id in regions_with_entrance_checks: for region_id in regions_with_entrance_checks:
region = regions_table[region_id] region = regions_table[region_id]
location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event")

View File

@@ -118,7 +118,7 @@ class L2ACWorld(World):
L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player)) L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player))
chest_access.show_in_spoiler = False chest_access.show_in_spoiler = False
ancient_dungeon.locations.append(chest_access) ancient_dungeon.locations.append(chest_access)
for iris in self.item_name_groups["Iris treasures"]: for iris in sorted(self.item_name_groups["Iris treasures"]):
treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}" treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
iris_treasure: Location = \ iris_treasure: Location = \
L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon) L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon)

View File

@@ -96,13 +96,13 @@ class MM2World(World):
location_name_groups = location_groups location_name_groups = location_groups
web = MM2WebWorld() web = MM2WebWorld()
rom_name: bytearray rom_name: bytearray
world_version: Tuple[int, int, int] = (0, 3, 1) world_version: Tuple[int, int, int] = (0, 3, 2)
wily_5_weapons: Dict[int, List[int]] wily_5_weapons: Dict[int, List[int]]
def __init__(self, world: MultiWorld, player: int): def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name = bytearray() self.rom_name = bytearray()
self.rom_name_available_event = threading.Event() self.rom_name_available_event = threading.Event()
super().__init__(world, player) super().__init__(multiworld, player)
self.weapon_damage = deepcopy(weapon_damage) self.weapon_damage = deepcopy(weapon_damage)
self.wily_5_weapons = {} self.wily_5_weapons = {}

View File

@@ -133,28 +133,6 @@ def set_rules(world: "MM2World") -> None:
# Wily Machine needs all three weaknesses present, so allow # Wily Machine needs all three weaknesses present, so allow
elif 4 > world.weapon_damage[weapon][i] > 0: elif 4 > world.weapon_damage[weapon][i] > 0:
world.weapon_damage[weapon][i] = 0 world.weapon_damage[weapon][i] = 0
# handle special cases
for boss in range(14):
for weapon in (1, 3, 6, 8):
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)):
# Weapon does not have enough possible ammo to kill the boss, raise the damage
if boss == 9:
if weapon != 3:
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
elif boss == 11:
if weapon == 1:
# Atomic Fire cannot be Boobeam Trap's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
else:
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
starting = world.options.starting_robot_master.value
world.weapon_damage[0][starting] = 1
for p_boss in world.options.plando_weakness: for p_boss in world.options.plando_weakness:
for p_weapon in world.options.plando_weakness[p_boss]: for p_weapon in world.options.plando_weakness[p_boss]:
@@ -168,6 +146,28 @@ def set_rules(world: "MM2World") -> None:
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
= world.options.plando_weakness[p_boss][p_weapon] = world.options.plando_weakness[p_boss][p_weapon]
# handle special cases
for boss in range(14):
for weapon in (1, 2, 3, 6, 8):
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
for i in range(9) if i != weapon)):
# Weapon does not have enough possible ammo to kill the boss, raise the damage
if boss == 9:
if weapon in (1, 6):
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
elif boss == 11:
if weapon == 1:
# Atomic Fire cannot be Boobeam Trap's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
else:
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value] world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
@@ -209,11 +209,11 @@ def set_rules(world: "MM2World") -> None:
continue continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp] uses = weapon_energy[wp] // weapon_costs[wp]
used_weapons[boss].add(wp)
if int(uses * boss_damage[wp]) > boss_health[boss]: if int(uses * boss_damage[wp]) > boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp]) used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0 boss_health[boss] = 0
used_weapons[boss].add(wp)
elif highest <= 0: elif highest <= 0:
# we are out of weapons that can actually damage the boss # we are out of weapons that can actually damage the boss
# so find the weapon that has the most uses, and apply that as an additional weakness # so find the weapon that has the most uses, and apply that as an additional weakness
@@ -221,18 +221,21 @@ def set_rules(world: "MM2World") -> None:
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should # Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
# be able to cover # be able to cover
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
if weapon != 0) if weapon != 0 and (weapon != 8 or boss != 12))
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
used = min(int(weapon_energy[wp] // weapon_costs[wp]), used = min(int(weapon_energy[wp] // weapon_costs[wp]),
ceil(boss_health[boss] // minimum_weakness_requirement[wp])) ceil(boss_health[boss] / minimum_weakness_requirement[wp]))
weapon_energy[wp] -= weapon_costs[wp] * used weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] -= int(used * minimum_weakness_requirement[wp]) boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
weapon_weight.pop(wp) weapon_weight.pop(wp)
used_weapons[boss].add(wp)
else: else:
# drain the weapon and continue # drain the weapon and continue
boss_health[boss] -= int(uses * boss_damage[wp]) boss_health[boss] -= int(uses * boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * uses weapon_energy[wp] -= weapon_costs[wp] * uses
weapon_weight.pop(wp) weapon_weight.pop(wp)
used_weapons[boss].add(wp)
world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons} world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons}

View File

@@ -1,7 +1,7 @@
from typing import DefaultDict from typing import DefaultDict
from collections import defaultdict from collections import defaultdict
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, { MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, {
' ': 0x40, ' ': 0x40,
'A': 0x41, 'A': 0x41,
'B': 0x42, 'B': 0x42,

View File

@@ -57,11 +57,11 @@ location_rows = [
LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12),
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0),
LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0),
LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),

View File

@@ -31,7 +31,7 @@ class RegionNames(str, Enum):
Mudskipper_Point = "Mudskipper Point" Mudskipper_Point = "Mudskipper Point"
Karamja = "Karamja" Karamja = "Karamja"
Corsair_Cove = "Corsair Cove" Corsair_Cove = "Corsair Cove"
Wilderness = "The Wilderness" Wilderness = "Wilderness"
Crandor = "Crandor" Crandor = "Crandor"
# Resource Regions # Resource Regions
Egg = "Egg" Egg = "Egg"

337
worlds/osrs/Rules.py Normal file
View File

@@ -0,0 +1,337 @@
"""
Ensures a target level can be reached with available resources
"""
from worlds.generic.Rules import CollectionRule, add_rule
from .Names import RegionNames, ItemNames
def get_fishing_skill_rule(level, player, options) -> CollectionRule:
if options.max_fishing_level < level:
return lambda state: False
if options.brutal_grinds or level < 5:
return lambda state: state.can_reach_region(RegionNames.Shrimp, player)
if level < 20:
return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \
state.can_reach_region(RegionNames.Port_Sarim, player)
else:
return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \
state.can_reach_region(RegionNames.Port_Sarim, player) and \
state.can_reach_region(RegionNames.Fly_Fish, player)
def get_mining_skill_rule(level, player, options) -> CollectionRule:
if options.max_mining_level < level:
return lambda state: False
if options.brutal_grinds or level < 15:
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \
state.can_reach_region(RegionNames.Clay_Rock, player)
else:
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or
state.can_reach_region(RegionNames.Clay_Rock, player)) and \
state.can_reach_region(RegionNames.Iron_Rock, player)
def get_woodcutting_skill_rule(level, player, options) -> CollectionRule:
if options.max_woodcutting_level < level:
return lambda state: False
if options.brutal_grinds or level < 15:
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
# Even the desert.
return lambda state: True
if level < 30:
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player)
else:
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \
state.can_reach_region(RegionNames.Willow_Tree, player)
def get_smithing_skill_rule(level, player, options) -> CollectionRule:
if options.max_smithing_level < level:
return lambda state: False
if options.brutal_grinds:
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Furnace, player)
if level < 15:
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
# in the "Anvil" resource region. We still need to check for it though.
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Furnace, player) and \
(state.can_reach_region(RegionNames.Anvil, player) or
state.can_reach_region(RegionNames.Lumbridge, player))
if level < 30:
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Iron_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and \
state.can_reach_region(RegionNames.Anvil, player)
else:
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Iron_Rock, player) and \
state.can_reach_region(RegionNames.Coal_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and \
state.can_reach_region(RegionNames.Anvil, player)
def get_crafting_skill_rule(level, player, options):
if options.max_crafting_level < level:
return lambda state: False
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
def can_spin(state):
return state.can_reach_region(RegionNames.Sheep, player) and \
state.can_reach_region(RegionNames.Spinning_Wheel, player)
def can_pot(state):
return state.can_reach_region(RegionNames.Clay_Rock, player) and \
state.can_reach_region(RegionNames.Barbarian_Village, player)
def can_tan(state):
return state.can_reach_region(RegionNames.Milk, player) and \
state.can_reach_region(RegionNames.Al_Kharid, player)
def mould_access(state):
return state.can_reach_region(RegionNames.Al_Kharid, player) or \
state.can_reach_region(RegionNames.Rimmington, player)
def can_silver(state):
return state.can_reach_region(RegionNames.Silver_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and mould_access(state)
def can_gold(state):
return state.can_reach_region(RegionNames.Gold_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and mould_access(state)
if options.brutal_grinds or level < 5:
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
can_smelt_gold = get_smithing_skill_rule(40, player, options)
can_smelt_silver = get_smithing_skill_rule(20, player, options)
if level < 16:
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
else:
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
(can_gold(state) and can_smelt_gold(state))
def get_cooking_skill_rule(level, player, options) -> CollectionRule:
if options.max_cooking_level < level:
return lambda state: False
if options.brutal_grinds or level < 15:
return lambda state: state.can_reach_region(RegionNames.Milk, player) or \
state.can_reach_region(RegionNames.Egg, player) or \
state.can_reach_region(RegionNames.Shrimp, player) or \
(state.can_reach_region(RegionNames.Wheat, player) and
state.can_reach_region(RegionNames.Windmill, player))
else:
can_catch_fly_fish = get_fishing_skill_rule(20, player, options)
return lambda state: (
(state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or
(state.can_reach_region(RegionNames.Port_Sarim, player))
) and (
state.can_reach_region(RegionNames.Milk, player) or
state.can_reach_region(RegionNames.Egg, player) or
state.can_reach_region(RegionNames.Shrimp, player) or
(state.can_reach_region(RegionNames.Wheat, player) and
state.can_reach_region(RegionNames.Windmill, player))
)
def get_runecraft_skill_rule(level, player, options) -> CollectionRule:
if options.max_runecraft_level < level:
return lambda state: False
if not options.brutal_grinds:
# Ensure access to the relevant altars
if level >= 5:
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player) and \
state.can_reach_region(RegionNames.Lumbridge_Swamp, player)
if level >= 9:
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player) and \
state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \
state.can_reach_region(RegionNames.East_Of_Varrock, player)
if level >= 14:
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player) and \
state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \
state.can_reach_region(RegionNames.East_Of_Varrock, player) and \
state.can_reach_region(RegionNames.Al_Kharid, player)
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player)
def get_magic_skill_rule(level, player, options) -> CollectionRule:
if options.max_magic_level < level:
return lambda state: False
return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player)
def get_firemaking_skill_rule(level, player, options) -> CollectionRule:
if options.max_firemaking_level < level:
return lambda state: False
if not options.brutal_grinds:
if level >= 30:
can_chop_willows = get_woodcutting_skill_rule(30, player, options)
return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state)
if level >= 15:
can_chop_oaks = get_woodcutting_skill_rule(15, player, options)
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state)
# If brutal grinds are on, or if the level is less than 15, you can train it.
return lambda state: True
def get_skill_rule(skill, level, player, options) -> CollectionRule:
if skill.lower() == "fishing":
return get_fishing_skill_rule(level, player, options)
if skill.lower() == "mining":
return get_mining_skill_rule(level, player, options)
if skill.lower() == "woodcutting":
return get_woodcutting_skill_rule(level, player, options)
if skill.lower() == "smithing":
return get_smithing_skill_rule(level, player, options)
if skill.lower() == "crafting":
return get_crafting_skill_rule(level, player, options)
if skill.lower() == "cooking":
return get_cooking_skill_rule(level, player, options)
if skill.lower() == "runecraft":
return get_runecraft_skill_rule(level, player, options)
if skill.lower() == "magic":
return get_magic_skill_rule(level, player, options)
if skill.lower() == "firemaking":
return get_firemaking_skill_rule(level, player, options)
return lambda state: True
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options):
if outbound_region_name == RegionNames.Cooks_Guild:
add_rule(entrance, get_cooking_skill_rule(32, player, options))
elif outbound_region_name == RegionNames.Crafting_Guild:
add_rule(entrance, get_crafting_skill_rule(40, player, options))
elif outbound_region_name == RegionNames.Corsair_Cove:
# Need to be able to start Corsair Curse in addition to having the item
add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player))
elif outbound_region_name == "Camdozaal*":
add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player))
elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
# Special logic for canoes
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
RegionNames.Edgeville, RegionNames.Wilderness]
if region_row.name in canoe_regions:
# Skill rules for greater distances
woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options)
woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options)
woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options)
woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options)
if region_row.name == RegionNames.Lumbridge:
# Canoe Tree access for the Location
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d3(state)) or
(state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_all(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d1)
elif outbound_region_name == RegionNames.Barbarian_Village:
add_rule(entrance, woodcutting_rule_d2)
elif outbound_region_name == RegionNames.Edgeville:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.Wilderness:
add_rule(entrance, woodcutting_rule_all)
elif region_row.name == RegionNames.South_Of_Varrock:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_d3(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_d1)
elif outbound_region_name == RegionNames.Barbarian_Village:
add_rule(entrance, woodcutting_rule_d1)
elif outbound_region_name == RegionNames.Edgeville:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.Wilderness:
add_rule(entrance, woodcutting_rule_all)
elif region_row.name == RegionNames.Barbarian_Village:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_d2(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_d2)
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d1)
# Edgeville does not need to be checked, because it's already adjacent
elif outbound_region_name == RegionNames.Wilderness:
add_rule(entrance, woodcutting_rule_d3)
elif region_row.name == RegionNames.Edgeville:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_d3(state)) or
(state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_d1(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d2)
# Barbarian Village does not need to be checked, because it's already adjacent
# Wilderness does not need to be checked, because it's already adjacent
elif region_row.name == RegionNames.Wilderness:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_all(state)) or
(state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d3(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d1(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_all)
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.Barbarian_Village:
add_rule(entrance, woodcutting_rule_d2)
# Edgeville does not need to be checked, because it's already adjacent

View File

@@ -1,12 +1,12 @@
import typing import typing
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState
from Fill import fill_restrictive, FillError
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_rule, CollectionRule
from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
chunksanity_special_region_names chunksanity_special_region_names
from .Locations import OSRSLocation, LocationRow from .Locations import OSRSLocation, LocationRow
from .Rules import *
from .Options import OSRSOptions, StartingArea from .Options import OSRSOptions, StartingArea
from .Names import LocationNames, ItemNames, RegionNames from .Names import LocationNames, ItemNames, RegionNames
@@ -46,6 +46,7 @@ class OSRSWorld(World):
web = OSRSWeb() web = OSRSWeb()
base_id = 0x070000 base_id = 0x070000
data_version = 1 data_version = 1
explicit_indirect_conditions = False
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))}
location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))}
@@ -61,6 +62,7 @@ class OSRSWorld(World):
starting_area_item: str starting_area_item: str
locations_by_category: typing.Dict[str, typing.List[LocationRow]] locations_by_category: typing.Dict[str, typing.List[LocationRow]]
available_QP_locations: typing.List[str]
def __init__(self, multiworld: MultiWorld, player: int): def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player) super().__init__(multiworld, player)
@@ -75,6 +77,7 @@ class OSRSWorld(World):
self.starting_area_item = "" self.starting_area_item = ""
self.locations_by_category = {} self.locations_by_category = {}
self.available_QP_locations = []
def generate_early(self) -> None: def generate_early(self) -> None:
location_categories = [location_row.category for location_row in location_rows] location_categories = [location_row.category for location_row in location_rows]
@@ -127,7 +130,6 @@ class OSRSWorld(World):
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region]) starting_entrance.connect(self.region_name_to_data[starting_area_region])
def create_regions(self) -> None: def create_regions(self) -> None:
""" """
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
@@ -145,7 +147,8 @@ class OSRSWorld(World):
# Removes the word "Area: " from the item name to get the region it applies to. # Removes the word "Area: " from the item name to get the region it applies to.
# I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse
if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it # if area hasn't been set, then we shouldn't connect it
if self.starting_area_item != "":
if self.starting_area_item in chunksanity_special_region_names: if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item] starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else: else:
@@ -164,11 +167,8 @@ class OSRSWorld(World):
entrance.connect(self.region_name_to_data[parsed_outbound]) entrance.connect(self.region_name_to_data[parsed_outbound])
item_name = self.region_rows_by_name[parsed_outbound].itemReq item_name = self.region_rows_by_name[parsed_outbound].itemReq
if "*" not in outbound_region_name and "*" not in item_name: entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player)
entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options)
continue
self.generate_special_rules_for(entrance, region_row, outbound_region_name)
for resource_region in region_row.resources: for resource_region in region_row.resources:
if not resource_region: if not resource_region:
@@ -178,321 +178,34 @@ class OSRSWorld(World):
if "*" not in resource_region: if "*" not in resource_region:
entrance.connect(self.region_name_to_data[resource_region]) entrance.connect(self.region_name_to_data[resource_region])
else: else:
self.generate_special_rules_for(entrance, region_row, resource_region)
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options)
self.roll_locations() self.roll_locations()
def generate_special_rules_for(self, entrance, region_row, outbound_region_name): def task_within_skill_levels(self, skills_required):
# print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") # Loop through each required skill. If any of its requirements are out of the defined limit, return false
if outbound_region_name == RegionNames.Cooks_Guild: for skill in skills_required:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level")
cooking_level_rule = self.get_skill_rule("cooking", 32) if skill.level > max_level_for_skill:
entrance.access_rule = lambda state: state.has(item_name, self.player) and \ return False
cooking_level_rule(state) return True
if self.options.brutal_grinds:
cooking_level_32_regions = {
RegionNames.Milk,
RegionNames.Egg,
RegionNames.Shrimp,
RegionNames.Wheat,
RegionNames.Windmill,
}
else:
# Level 15 cooking and higher requires level 20 fishing.
fishing_level_20_regions = {
RegionNames.Shrimp,
RegionNames.Port_Sarim,
}
cooking_level_32_regions = {
RegionNames.Milk,
RegionNames.Egg,
RegionNames.Shrimp,
RegionNames.Wheat,
RegionNames.Windmill,
RegionNames.Fly_Fish,
*fishing_level_20_regions,
}
for region_name in cooking_level_32_regions:
self.multiworld.register_indirect_condition(self.get_region(region_name), entrance)
return
if outbound_region_name == RegionNames.Crafting_Guild:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
crafting_level_rule = self.get_skill_rule("crafting", 40)
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
crafting_level_rule(state)
if self.options.brutal_grinds:
crafting_level_40_regions = {
# can_spin
RegionNames.Sheep,
RegionNames.Spinning_Wheel,
# can_pot
RegionNames.Clay_Rock,
RegionNames.Barbarian_Village,
# can_tan
RegionNames.Milk,
RegionNames.Al_Kharid,
}
else:
mould_access_regions = {
RegionNames.Al_Kharid,
RegionNames.Rimmington,
}
smithing_level_20_regions = {
RegionNames.Bronze_Ores,
RegionNames.Iron_Rock,
RegionNames.Furnace,
RegionNames.Anvil,
}
smithing_level_40_regions = {
*smithing_level_20_regions,
RegionNames.Coal_Rock,
}
crafting_level_40_regions = {
# can_tan
RegionNames.Milk,
RegionNames.Al_Kharid,
# can_silver
RegionNames.Silver_Rock,
RegionNames.Furnace,
*mould_access_regions,
# can_smelt_silver
*smithing_level_20_regions,
# can_gold
RegionNames.Gold_Rock,
RegionNames.Furnace,
*mould_access_regions,
# can_smelt_gold
*smithing_level_40_regions,
}
for region_name in crafting_level_40_regions:
self.multiworld.register_indirect_condition(self.get_region(region_name), entrance)
return
if outbound_region_name == RegionNames.Corsair_Cove:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
# Need to be able to start Corsair Curse in addition to having the item
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
state.can_reach(RegionNames.Falador_Farm, "Region", self.player)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance)
return
if outbound_region_name == "Camdozaal*":
item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
state.has(ItemNames.QP_Below_Ice_Mountain, self.player)
return
if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player)
return
# Special logic for canoes
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
RegionNames.Edgeville, RegionNames.Wilderness]
if region_row.name in canoe_regions:
# Skill rules for greater distances
woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12)
woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27)
woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42)
woodcutting_rule_all = self.get_skill_rule("woodcutting", 57)
def add_indirect_conditions_for_woodcutting_levels(entrance, *levels: int):
if self.options.brutal_grinds:
# No access to specific regions required.
return
# Currently, each level requirement requires everything from the previous level requirements, so the
# maximum level requirement can be taken.
max_level = max(levels, default=0)
max_level = min(max_level, self.options.max_woodcutting_level.value)
if 15 <= max_level < 30:
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance)
elif 30 <= max_level:
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance)
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Willow_Tree), entrance)
if region_row.name == RegionNames.Lumbridge:
# Canoe Tree access for the Location
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
# South of Varrock does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
if outbound_region_name == RegionNames.Edgeville:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 57
add_indirect_conditions_for_woodcutting_levels(entrance, 57)
if region_row.name == RegionNames.South_Of_Varrock:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
# Lumbridge does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
and self.options.max_woodcutting_level >= 12
add_indirect_conditions_for_woodcutting_levels(entrance, 12)
if outbound_region_name == RegionNames.Edgeville:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if region_row.name == RegionNames.Barbarian_Village:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
and self.options.max_woodcutting_level >= 12
add_indirect_conditions_for_woodcutting_levels(entrance, 12)
# Edgeville does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if region_row.name == RegionNames.Edgeville:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
# Barbarian Village does not need to be checked, because it's already adjacent
# Wilderness does not need to be checked, because it's already adjacent
if region_row.name == RegionNames.Wilderness:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 57
add_indirect_conditions_for_woodcutting_levels(entrance, 57)
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
# Edgeville does not need to be checked, because it's already adjacent
def roll_locations(self): def roll_locations(self):
locations_required = 0
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
locations_required = 0
for item_row in item_rows: for item_row in item_rows:
locations_required += item_row.amount locations_required += item_row.amount
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
# Quests are always added # Quests are always added first, before anything else is rolled
for i, location_row in enumerate(location_rows): for i, location_row in enumerate(location_rows):
if location_row.category in {"quest", "points", "goal"}: if location_row.category in {"quest", "points", "goal"}:
self.create_and_add_location(i) if self.task_within_skill_levels(location_row.skills):
if location_row.category == "quest": self.create_and_add_location(i)
locations_added += 1 if location_row.category == "quest":
locations_added += 1
# Build up the weighted Task Pool # Build up the weighted Task Pool
rnd = self.random rnd = self.random
@@ -516,10 +229,9 @@ class OSRSWorld(World):
task_types = ["prayer", "magic", "runecraft", "mining", "crafting", task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
for task_type in task_types: for task_type in task_types:
max_level_for_task_type = getattr(self.options, f"max_{task_type}_level")
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
tasks_for_this_type = [task for task in self.locations_by_category[task_type] tasks_for_this_type = [task for task in self.locations_by_category[task_type]
if task.skills[0].level <= max_level_for_task_type] if self.task_within_skill_levels(task.skills)]
if not self.options.progressive_tasks: if not self.options.progressive_tasks:
rnd.shuffle(tasks_for_this_type) rnd.shuffle(tasks_for_this_type)
else: else:
@@ -568,6 +280,7 @@ class OSRSWorld(World):
self.add_location(task) self.add_location(task)
locations_added += 1 locations_added += 1
def add_location(self, location): def add_location(self, location):
index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0]
self.create_and_add_location(index) self.create_and_add_location(index)
@@ -586,11 +299,15 @@ class OSRSWorld(World):
def create_and_add_location(self, row_index) -> None: def create_and_add_location(self, row_index) -> None:
location_row = location_rows[row_index] location_row = location_rows[row_index]
# print(f"Adding task {location_row.name}")
# Quest Points are handled differently now, but in case this gets fed an older version of the data sheet,
# the points might still be listed in a different row
if location_row.category == "points":
return
# Create Location # Create Location
location_id = self.base_id + row_index location_id = self.base_id + row_index
if location_row.category == "points" or location_row.category == "goal": if location_row.category == "goal":
location_id = None location_id = None
location = OSRSLocation(self.player, location_row.name, location_id) location = OSRSLocation(self.player, location_row.name, location_id)
self.location_name_to_data[location_row.name] = location self.location_name_to_data[location_row.name] = location
@@ -602,6 +319,14 @@ class OSRSWorld(World):
location.parent_region = region location.parent_region = region
region.locations.append(location) region.locations.append(location)
# If it's a quest, generate a "Points" location we'll add an event to
if location_row.category == "quest":
points_name = location_row.name.replace("Quest:", "Points:")
points_location = OSRSLocation(self.player, points_name)
self.location_name_to_data[points_name] = points_location
points_location.parent_region = region
region.locations.append(points_location)
def set_rules(self) -> None: def set_rules(self) -> None:
""" """
called to set access and item rules on locations and entrances. called to set access and item rules on locations and entrances.
@@ -612,18 +337,26 @@ class OSRSWorld(World):
"Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure",
"Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot",
"Below_Ice_Mountain"] "Below_Ice_Mountain"]
for qp_attr_name in quest_attr_names:
loc_name = getattr(LocationNames, f"QP_{qp_attr_name}")
item_name = getattr(ItemNames, f"QP_{qp_attr_name}")
self.multiworld.get_location(loc_name, self.player) \
.place_locked_item(self.create_event(item_name))
for quest_attr_name in quest_attr_names: for quest_attr_name in quest_attr_names:
qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}")
qp_loc = self.location_name_to_data.get(qp_loc_name)
q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}")
add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( q_loc = self.location_name_to_data.get(q_loc_name)
self.multiworld.get_location(q_loc_name, self.player).can_reach(state)
)) # Checks to make sure the task is actually in the list before trying to create its rules
if qp_loc and q_loc:
# Create the QP Event Item
item_name = getattr(ItemNames, f"QP_{quest_attr_name}")
qp_loc.place_locked_item(self.create_event(item_name))
# If a quest is excluded, don't actually consider it for quest point progression
if q_loc_name not in self.options.exclude_locations:
self.available_QP_locations.append(item_name)
# Set the access rule for the QP Location
add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state)))
# place "Victory" at "Dragon Slayer" and set collection as win condition # place "Victory" at "Dragon Slayer" and set collection as win condition
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \
@@ -639,7 +372,7 @@ class OSRSWorld(World):
lambda state, region_required=region_required: state.can_reach(region_required, "Region", lambda state, region_required=region_required: state.can_reach(region_required, "Region",
self.player)) self.player))
for skill_req in location_row.skills: for skill_req in location_row.skills:
add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options))
for item_req in location_row.items: for item_req in location_row.items:
add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player))
if location_row.qp: if location_row.qp:
@@ -664,124 +397,8 @@ class OSRSWorld(World):
def quest_points(self, state): def quest_points(self, state):
qp = 0 qp = 0
for qp_event in QP_Items: for qp_event in self.available_QP_locations:
if state.has(qp_event, self.player): if state.has(qp_event, self.player):
qp += int(qp_event[0]) qp += int(qp_event[0])
return qp return qp
"""
Ensures a target level can be reached with available resources
"""
def get_skill_rule(self, skill, level) -> CollectionRule:
if skill.lower() == "fishing":
if self.options.brutal_grinds or level < 5:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player)
if level < 20:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
state.can_reach(RegionNames.Port_Sarim, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \
state.can_reach(RegionNames.Fly_Fish, "Region", self.player)
if skill.lower() == "mining":
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)
else:
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player)
if skill.lower() == "woodcutting":
if self.options.brutal_grinds or level < 15:
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
# Even the desert.
return lambda state: True
if level < 30:
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \
state.can_reach(RegionNames.Willow_Tree, "Region", self.player)
if skill.lower() == "smithing":
if self.options.brutal_grinds:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player)
if level < 15:
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
# in the "Anvil" resource region. We still need to check for it though.
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
(state.can_reach(RegionNames.Anvil, "Region", self.player) or
state.can_reach(RegionNames.Lumbridge, "Region", self.player))
if level < 30:
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
state.can_reach(RegionNames.Anvil, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
state.can_reach(RegionNames.Anvil, "Region", self.player)
if skill.lower() == "crafting":
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
def can_spin(state):
return state.can_reach(RegionNames.Sheep, "Region", self.player) and \
state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player)
def can_pot(state):
return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Barbarian_Village, "Region", self.player)
def can_tan(state):
return state.can_reach(RegionNames.Milk, "Region", self.player) and \
state.can_reach(RegionNames.Al_Kharid, "Region", self.player)
def mould_access(state):
return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \
state.can_reach(RegionNames.Rimmington, "Region", self.player)
def can_silver(state):
return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
def can_gold(state):
return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
if self.options.brutal_grinds or level < 5:
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
can_smelt_gold = self.get_skill_rule("smithing", 40)
can_smelt_silver = self.get_skill_rule("smithing", 20)
if level < 16:
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
else:
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
(can_gold(state) and can_smelt_gold(state))
if skill.lower() == "cooking":
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \
state.can_reach(RegionNames.Egg, "Region", self.player) or \
state.can_reach(RegionNames.Shrimp, "Region", self.player) or \
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
state.can_reach(RegionNames.Windmill, "Region", self.player))
else:
can_catch_fly_fish = self.get_skill_rule("fishing", 20)
return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \
can_catch_fly_fish(state) and \
(state.can_reach(RegionNames.Milk, "Region", self.player) or
state.can_reach(RegionNames.Egg, "Region", self.player) or
state.can_reach(RegionNames.Shrimp, "Region", self.player) or
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
state.can_reach(RegionNames.Windmill, "Region", self.player)))
if skill.lower() == "runecraft":
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player)
if skill.lower() == "magic":
return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player)
return lambda state: True

View File

@@ -1387,7 +1387,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]:
lambda state: logic.templars_return_requirement(state)), lambda state: logic.templars_return_requirement(state)),
LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY, LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
lambda state: logic.the_host_requirement(state)), lambda state: logic.the_host_requirement(state)),
LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.VICTORY, LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.EXTRA,
lambda state: logic.the_host_requirement(state)), lambda state: logic.the_host_requirement(state)),
LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA, LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA,
lambda state: logic.the_host_requirement(state)), lambda state: logic.the_host_requirement(state)),

View File

@@ -43,6 +43,9 @@ class SC2Campaign(Enum):
self.goal_priority = goal_priority self.goal_priority = goal_priority
self.race = race self.race = race
def __lt__(self, other: "SC2Campaign"):
return self.id < other.id
GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY
WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN
PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS

View File

@@ -50,7 +50,7 @@ def create_vanilla_regions(
names: Dict[str, int] = {} names: Dict[str, int] = {}
# Generating all regions and locations for each enabled campaign # Generating all regions and locations for each enabled campaign
for campaign in enabled_campaigns: for campaign in sorted(enabled_campaigns):
for region_name in vanilla_mission_req_table[campaign].keys(): for region_name in vanilla_mission_req_table[campaign].keys():
regions.append(create_region(world, locations_per_region, location_cache, region_name)) regions.append(create_region(world, locations_per_region, location_cache, region_name))
world.multiworld.regions += regions world.multiworld.regions += regions

View File

@@ -41,9 +41,7 @@ all_random_settings = {
Friendsanity.internal_name: "random", Friendsanity.internal_name: "random",
FriendsanityHeartSize.internal_name: "random", FriendsanityHeartSize.internal_name: "random",
Booksanity.internal_name: "random", Booksanity.internal_name: "random",
Walnutsanity.internal_name: "random",
NumberOfMovementBuffs.internal_name: "random", NumberOfMovementBuffs.internal_name: "random",
EnabledFillerBuffs.internal_name: "random",
ExcludeGingerIsland.internal_name: "random", ExcludeGingerIsland.internal_name: "random",
TrapItems.internal_name: "random", TrapItems.internal_name: "random",
MultipleDaySleepEnabled.internal_name: "random", MultipleDaySleepEnabled.internal_name: "random",

View File

@@ -112,8 +112,7 @@ class AggressiveScanLogic(Choice):
class SubnauticaDeathLink(DeathLink): class SubnauticaDeathLink(DeathLink):
"""When you die, everyone dies. Of course the reverse is true too. __doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"."
Note: can be toggled via in-game console command "deathlink"."""
class FillerItemsDistribution(ItemDict): class FillerItemsDistribution(ItemDict):

View File

@@ -379,6 +379,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
cantoran: Cantoran cantoran: Cantoran
lore_checks: LoreChecks lore_checks: LoreChecks
boss_rando: BossRando boss_rando: BossRando
enemy_rando: EnemyRando
damage_rando: DamageRando damage_rando: DamageRando
damage_rando_overrides: DamageRandoOverrides damage_rando_overrides: DamageRandoOverrides
hp_cap: HpCap hp_cap: HpCap
@@ -445,6 +446,7 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
Cantoran: hidden(Cantoran) # type: ignore Cantoran: hidden(Cantoran) # type: ignore
LoreChecks: hidden(LoreChecks) # type: ignore LoreChecks: hidden(LoreChecks) # type: ignore
BossRando: hidden(BossRando) # type: ignore BossRando: hidden(BossRando) # type: ignore
EnemyRando: hidden(EnemyRando) # type: ignore
DamageRando: hidden(DamageRando) # type: ignore DamageRando: hidden(DamageRando) # type: ignore
DamageRandoOverrides: HiddenDamageRandoOverrides DamageRandoOverrides: HiddenDamageRandoOverrides
HpCap: hidden(HpCap) # type: ignore HpCap: hidden(HpCap) # type: ignore
@@ -516,6 +518,10 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
self.boss_rando == BossRando.default: self.boss_rando == BossRando.default:
self.boss_rando.value = self.BossRando.value self.boss_rando.value = self.BossRando.value
self.has_replaced_options.value = Toggle.option_true self.has_replaced_options.value = Toggle.option_true
if self.EnemyRando != EnemyRando.default and \
self.enemy_rando == EnemyRando.default:
self.enemy_rando.value = self.EnemyRando.value
self.has_replaced_options.value = Toggle.option_true
if self.DamageRando != DamageRando.default and \ if self.DamageRando != DamageRando.default and \
self.damage_rando == DamageRando.default: self.damage_rando == DamageRando.default:
self.damage_rando.value = self.DamageRando.value self.damage_rando.value = self.DamageRando.value

View File

@@ -98,6 +98,7 @@ class TimespinnerWorld(World):
"Cantoran": self.options.cantoran.value, "Cantoran": self.options.cantoran.value,
"LoreChecks": self.options.lore_checks.value, "LoreChecks": self.options.lore_checks.value,
"BossRando": self.options.boss_rando.value, "BossRando": self.options.boss_rando.value,
"EnemyRando": self.options.enemy_rando.value,
"DamageRando": self.options.damage_rando.value, "DamageRando": self.options.damage_rando.value,
"DamageRandoOverrides": self.options.damage_rando_overrides.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value,
"HpCap": self.options.hp_cap.value, "HpCap": self.options.hp_cap.value,

View File

@@ -108,11 +108,15 @@ sword_cave_locations = [
] ]
food_locations = [ food_locations = [
"Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
"Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)",
"Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)"
] ]
gohma_locations = [
"Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)"
]
gleeok_locations = [ gleeok_locations = [
"Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce" "Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce"
] ]

View File

@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from worlds.generic.Rules import add_rule from worlds.generic.Rules import add_rule
from .Locations import food_locations, shop_locations, gleeok_locations from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations
from .ItemPool import dangerous_weapon_locations from .ItemPool import dangerous_weapon_locations
from .Options import StartingPosition from .Options import StartingPosition
@@ -10,13 +10,12 @@ if TYPE_CHECKING:
def set_rules(tloz_world: "TLoZWorld"): def set_rules(tloz_world: "TLoZWorld"):
player = tloz_world.player player = tloz_world.player
world = tloz_world.multiworld
options = tloz_world.options options = tloz_world.options
# Boss events for a nicer spoiler log play through # Boss events for a nicer spoiler log play through
for level in range(1, 9): for level in range(1, 9):
boss = world.get_location(f"Level {level} Boss", player) boss = tloz_world.get_location(f"Level {level} Boss")
boss_event = world.get_location(f"Level {level} Boss Status", player) boss_event = tloz_world.get_location(f"Level {level} Boss Status")
status = tloz_world.create_event(f"Boss {level} Defeated") status = tloz_world.create_event(f"Boss {level} Defeated")
boss_event.place_locked_item(status) boss_event.place_locked_item(status)
add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player))
@@ -26,136 +25,131 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in level.locations: for location in level.locations:
if options.StartingPosition < StartingPosition.option_dangerous \ if options.StartingPosition < StartingPosition.option_dangerous \
or location.name not in dangerous_weapon_locations: or location.name not in dangerous_weapon_locations:
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has_group("weapons", player)) lambda state: state.has_group("weapons", player))
# This part of the loop sets up an expected amount of defense needed for each dungeon # This part of the loop sets up an expected amount of defense needed for each dungeon
if i > 0: # Don't need an extra heart for Level 1 if i > 0: # Don't need an extra heart for Level 1
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state, hearts=i: state.has("Heart Container", player, hearts) or lambda state, hearts=i: state.has("Heart Container", player, hearts) or
(state.has("Blue Ring", player) and (state.has("Blue Ring", player) and
state.has("Heart Container", player, int(hearts / 2))) or state.has("Heart Container", player, int(hearts / 2))) or
(state.has("Red Ring", player) and (state.has("Red Ring", player) and
state.has("Heart Container", player, int(hearts / 4)))) state.has("Heart Container", player, int(hearts / 4))))
if "Pols Voice" in location.name: # This enemy needs specific weapons if "Pols Voice" in location.name: # This enemy needs specific weapons
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has_group("swords", player) or state.has("Bow", player)) lambda state: state.has_group("swords", player) or
(state.has("Bow", player) and state.has_group("arrows", player)))
# No requiring anything in a shop until we can farm for money # No requiring anything in a shop until we can farm for money
for location in shop_locations: for location in shop_locations:
add_rule(world.get_location(location, player), add_rule(tloz_world.get_location(location),
lambda state: state.has_group("weapons", player)) lambda state: state.has_group("weapons", player))
# Everything from 4 on up has dark rooms # Everything from 4 on up has dark rooms
for level in tloz_world.levels[4:]: for level in tloz_world.levels[4:]:
for location in level.locations: for location in level.locations:
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has_group("candles", player) lambda state: state.has_group("candles", player)
or (state.has("Magical Rod", player) and state.has("Book of Magic", player))) or (state.has("Magical Rod", player) and state.has("Book of Magic", player)))
# Everything from 5 on up has gaps # Everything from 5 on up has gaps
for level in tloz_world.levels[5:]: for level in tloz_world.levels[5:]:
for location in level.locations: for location in level.locations:
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has("Stepladder", player)) lambda state: state.has("Stepladder", player))
add_rule(world.get_location("Level 5 Boss", player), # Level 4 Access
for location in tloz_world.levels[4].locations:
add_rule(tloz_world.get_location(location.name),
lambda state: state.has_any(("Raft", "Recorder"), player))
# Digdogger boss. Rework this once ER happens
add_rule(tloz_world.get_location("Level 5 Boss"),
lambda state: state.has("Recorder", player))
add_rule(tloz_world.get_location("Level 5 Triforce"),
lambda state: state.has("Recorder", player)) lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 6 Boss", player), for location in gohma_locations:
lambda state: state.has("Bow", player) and state.has_group("arrows", player)) if options.ExpandedPool or "Drop" not in location:
add_rule(tloz_world.get_location(location),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
add_rule(world.get_location("Level 7 Item (Red Candle)", player), # Recorder Access for Level 7
lambda state: state.has("Recorder", player)) for location in tloz_world.levels[7].locations:
add_rule(world.get_location("Level 7 Boss", player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has("Recorder", player))
if options.ExpandedPool:
add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player),
lambda state: state.has("Recorder", player)) lambda state: state.has("Recorder", player))
for location in food_locations: for location in food_locations:
if options.ExpandedPool or "Drop" not in location: if options.ExpandedPool or "Drop" not in location:
add_rule(world.get_location(location, player), add_rule(tloz_world.get_location(location),
lambda state: state.has("Food", player)) lambda state: state.has("Food", player))
for location in gleeok_locations: for location in gleeok_locations:
add_rule(world.get_location(location, player), add_rule(tloz_world.get_location(location),
lambda state: state.has_group("swords", player) or state.has("Magical Rod", player)) lambda state: state.has_group("swords", player) or state.has("Magical Rod", player))
# Candle access for Level 8 # Candle access for Level 8
for location in tloz_world.levels[8].locations: for location in tloz_world.levels[8].locations:
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has_group("candles", player)) lambda state: state.has_group("candles", player))
add_rule(world.get_location("Level 8 Item (Magical Key)", player), add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"),
lambda state: state.has("Bow", player) and state.has_group("arrows", player)) lambda state: state.has("Bow", player) and state.has_group("arrows", player))
if options.ExpandedPool: if options.ExpandedPool:
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"),
lambda state: state.has("Bow", player) and state.has_group("arrows", player)) lambda state: state.has("Bow", player) and state.has_group("arrows", player))
for location in tloz_world.levels[9].locations: for location in tloz_world.levels[9].locations:
add_rule(world.get_location(location.name, player), add_rule(tloz_world.get_location(location.name),
lambda state: state.has("Triforce Fragment", player, 8) and lambda state: state.has("Triforce Fragment", player, 8) and
state.has_group("swords", player)) state.has_group("swords", player))
# Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop
for level in range(1, 9): for level in range(1, 9):
add_rule(world.get_location(f"Level {level} Triforce", player), add_rule(tloz_world.get_location(f"Level {level} Triforce"),
lambda state, l=level: state.has(f"Boss {l} Defeated", player)) lambda state, l=level: state.has(f"Boss {l} Defeated", player))
# Sword, raft, and ladder spots # Sword, raft, and ladder spots
add_rule(world.get_location("White Sword Pond", player), add_rule(tloz_world.get_location("White Sword Pond"),
lambda state: state.has("Heart Container", player, 2)) lambda state: state.has("Heart Container", player, 2))
add_rule(world.get_location("Magical Sword Grave", player), add_rule(tloz_world.get_location("Magical Sword Grave"),
lambda state: state.has("Heart Container", player, 9)) lambda state: state.has("Heart Container", player, 9))
stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"]
stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"]
for location in stepladder_locations: for location in stepladder_locations:
add_rule(world.get_location(location, player), add_rule(tloz_world.get_location(location),
lambda state: state.has("Stepladder", player)) lambda state: state.has("Stepladder", player))
if options.ExpandedPool: if options.ExpandedPool:
for location in stepladder_locations_expanded: for location in stepladder_locations_expanded:
add_rule(world.get_location(location, player), add_rule(tloz_world.get_location(location),
lambda state: state.has("Stepladder", player)) lambda state: state.has("Stepladder", player))
# Don't allow Take Any Items until we can actually get in one # Don't allow Take Any Items until we can actually get in one
if options.ExpandedPool: if options.ExpandedPool:
add_rule(world.get_location("Take Any Item Left", player), add_rule(tloz_world.get_location("Take Any Item Left"),
lambda state: state.has_group("candles", player) or lambda state: state.has_group("candles", player) or
state.has("Raft", player)) state.has("Raft", player))
add_rule(world.get_location("Take Any Item Middle", player), add_rule(tloz_world.get_location("Take Any Item Middle"),
lambda state: state.has_group("candles", player) or lambda state: state.has_group("candles", player) or
state.has("Raft", player)) state.has("Raft", player))
add_rule(world.get_location("Take Any Item Right", player), add_rule(tloz_world.get_location("Take Any Item Right"),
lambda state: state.has_group("candles", player) or lambda state: state.has_group("candles", player) or
state.has("Raft", player)) state.has("Raft", player))
for location in tloz_world.levels[4].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has("Raft", player) or state.has("Recorder", player))
for location in tloz_world.levels[7].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has("Recorder", player))
for location in tloz_world.levels[8].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has("Bow", player))
add_rule(world.get_location("Potion Shop Item Left", player), add_rule(tloz_world.get_location("Potion Shop Item Left"),
lambda state: state.has("Letter", player)) lambda state: state.has("Letter", player))
add_rule(world.get_location("Potion Shop Item Middle", player), add_rule(tloz_world.get_location("Potion Shop Item Middle"),
lambda state: state.has("Letter", player)) lambda state: state.has("Letter", player))
add_rule(world.get_location("Potion Shop Item Right", player), add_rule(tloz_world.get_location("Potion Shop Item Right"),
lambda state: state.has("Letter", player)) lambda state: state.has("Letter", player))
add_rule(world.get_location("Shield Shop Item Left", player), add_rule(tloz_world.get_location("Shield Shop Item Left"),
lambda state: state.has_group("candles", player) or lambda state: state.has_group("candles", player) or
state.has("Bomb", player)) state.has("Bomb", player))
add_rule(world.get_location("Shield Shop Item Middle", player), add_rule(tloz_world.get_location("Shield Shop Item Middle"),
lambda state: state.has_group("candles", player) or lambda state: state.has_group("candles", player) or
state.has("Bomb", player)) state.has("Bomb", player))
add_rule(world.get_location("Shield Shop Item Right", player), add_rule(tloz_world.get_location("Shield Shop Item Right"),
lambda state: state.has_group("candles", player) or lambda state: state.has_group("candles", player) or
state.has("Bomb", player)) state.has("Bomb", player))

View File

@@ -83,6 +83,11 @@ class TunicWorld(World):
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
# so we only loop the multiworld locations once
# if these are locations instead of their info, it gives a memory leak error
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
player_item_link_locations: Dict[str, List[Location]]
def generate_early(self) -> None: def generate_early(self) -> None:
if self.options.logic_rules >= LogicRules.option_no_major_glitches: if self.options.logic_rules >= LogicRules.option_no_major_glitches:
self.options.laurels_zips.value = LaurelsZips.option_true self.options.laurels_zips.value = LaurelsZips.option_true
@@ -387,6 +392,18 @@ class TunicWorld(World):
if hint_text: if hint_text:
hint_data[self.player][location.address] = hint_text hint_data[self.player][location.address] = hint_text
def get_real_location(self, location: Location) -> Tuple[str, int]:
# if it's not in a group, it's not in an item link
if location.player not in self.multiworld.groups or not location.item:
return location.name, location.player
try:
loc = self.player_item_link_locations[location.item.name].pop()
return loc.name, loc.player
except IndexError:
warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. "
f"Using a potentially incorrect location name instead.")
return location.name, location.player
def fill_slot_data(self) -> Dict[str, Any]: def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = { slot_data: Dict[str, Any] = {
"seed": self.random.randint(0, 2147483647), "seed": self.random.randint(0, 2147483647),
@@ -412,12 +429,35 @@ class TunicWorld(World):
"disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race),
} }
# this would be in a stage if there was an appropriate stage for it
self.player_item_link_locations = {}
groups = self.multiworld.get_player_groups(self.player)
# checking if groups so that this doesn't run if the player isn't in a group
if groups:
if not self.item_link_locations:
tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC")
# figure out our groups and the items in them
for tunic in tunic_worlds:
for group in self.multiworld.get_player_groups(tunic.player):
self.item_link_locations.setdefault(group, {})
for location in self.multiworld.get_locations():
if location.item and location.item.player in self.item_link_locations.keys():
(self.item_link_locations[location.item.player].setdefault(location.item.name, [])
.append((location.player, location.name)))
# if item links are on, set up the player's personal item link locations, so we can pop them as needed
for group, item_links in self.item_link_locations.items():
if group in groups:
for item_name, locs in item_links.items():
self.player_item_link_locations[item_name] = \
[self.multiworld.get_location(location_name, player) for player, location_name in locs]
for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items):
if tunic_item.name not in slot_data: if tunic_item.name not in slot_data:
slot_data[tunic_item.name] = [] slot_data[tunic_item.name] = []
if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6:
continue continue
slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location))
for start_item in self.options.start_inventory_from_pool: for start_item in self.options.start_inventory_from_pool:
if start_item in slot_data_item_names: if start_item in slot_data_item_names:
@@ -436,7 +476,7 @@ class TunicWorld(World):
if item in slot_data_item_names: if item in slot_data_item_names:
slot_data[item] = [] slot_data[item] = []
for item_location in self.multiworld.find_item_locations(item, self.player): for item_location in self.multiworld.find_item_locations(item, self.player):
slot_data[item].extend([item_location.name, item_location.player]) slot_data[item].extend(self.get_real_location(item_location))
return slot_data return slot_data

View File

@@ -807,7 +807,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[], [],
# drop a rudeling, icebolt or ice bomb # drop a rudeling, icebolt or ice bomb
"Overworld to West Garden from Furnace": "Overworld to West Garden from Furnace":
[["IG3"]], [["IG3"], ["LS1"]],
}, },
"East Overworld": { "East Overworld": {
"Above Ruined Passage": "Above Ruined Passage":

View File

@@ -501,9 +501,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Dark Tomb Upper"].connect( regions["Dark Tomb Upper"].connect(
connecting_region=regions["Dark Tomb Entry Point"]) connecting_region=regions["Dark Tomb Entry Point"])
# ice grapple through the wall, get the little secret sound to trigger
regions["Dark Tomb Upper"].connect( regions["Dark Tomb Upper"].connect(
connecting_region=regions["Dark Tomb Main"], connecting_region=regions["Dark Tomb Main"],
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Dark Tomb Main"].connect( regions["Dark Tomb Main"].connect(
connecting_region=regions["Dark Tomb Upper"], connecting_region=regions["Dark Tomb Upper"],
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
@@ -779,12 +781,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Fortress East Shortcut Upper"].connect( regions["Fortress East Shortcut Upper"].connect(
connecting_region=regions["Fortress East Shortcut Lower"]) connecting_region=regions["Fortress East Shortcut Lower"])
# nmg: can ice grapple upwards
regions["Fortress East Shortcut Lower"].connect( regions["Fortress East Shortcut Lower"].connect(
connecting_region=regions["Fortress East Shortcut Upper"], connecting_region=regions["Fortress East Shortcut Upper"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
# nmg: ice grapple through the big gold door, can do it both ways
regions["Eastern Vault Fortress"].connect( regions["Eastern Vault Fortress"].connect(
connecting_region=regions["Eastern Vault Fortress Gold Door"], connecting_region=regions["Eastern Vault Fortress Gold Door"],
rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
@@ -807,7 +807,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Fortress Hero's Grave Region"].connect( regions["Fortress Hero's Grave Region"].connect(
connecting_region=regions["Fortress Grave Path"]) connecting_region=regions["Fortress Grave Path"])
# nmg: ice grapple from upper grave path to lower
regions["Fortress Grave Path Upper"].connect( regions["Fortress Grave Path Upper"].connect(
connecting_region=regions["Fortress Grave Path"], connecting_region=regions["Fortress Grave Path"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
@@ -1139,6 +1138,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
for portal_dest in region_info.portals: for portal_dest in region_info.portals:
ls_connect(ladder_region, "Overworld Redux, " + portal_dest) ls_connect(ladder_region, "Overworld Redux, " + portal_dest)
# convenient staircase means this one is easy difficulty, even though there's an elevation change
ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west")
# connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail
if options.ladder_storage >= LadderStorage.option_medium: if options.ladder_storage >= LadderStorage.option_medium:
for ladder_region, region_info in ow_ladder_groups.items(): for ladder_region, region_info in ow_ladder_groups.items():
@@ -1154,6 +1156,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
if options.ladder_storage >= LadderStorage.option_hard: if options.ladder_storage >= LadderStorage.option_hard:
ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_")
ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_")
ls_connect("LS Elev 2", "Overworld Redux, Ruins Passage_west")
ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house")
ls_connect("LS Elev 5", "Overworld Redux, Temple_main") ls_connect("LS Elev 5", "Overworld Redux, Temple_main")

View File

@@ -17,7 +17,7 @@ ow_ladder_groups: Dict[str, OWLadderInfo] = {
["Overworld Beach"]), ["Overworld Beach"]),
# also the east filigree room # also the east filigree room
"LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"},
["Furnace_gyro_lower", "Swamp Redux 2_wall"], ["Furnace_gyro_lower", "Furnace_gyro_west", "Swamp Redux 2_wall"],
["Overworld Tunnel Turret"]), ["Overworld Tunnel Turret"]),
# also the fountain filigree room and ruined passage door # also the fountain filigree room and ruined passage door
"LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"},

View File

@@ -80,7 +80,7 @@ class WitnessWorld(World):
def _get_slot_data(self) -> Dict[str, Any]: def _get_slot_data(self) -> Dict[str, Any]:
return { return {
"seed": self.random.randrange(0, 1000000), "seed": self.options.puzzle_randomization_seed.value,
"victory_location": int(self.player_logic.VICTORY_LOCATION, 16), "victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
"panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
"item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),

View File

@@ -401,6 +401,16 @@ class DeathLinkAmnesty(Range):
default = 1 default = 1
class PuzzleRandomizationSeed(Range):
"""
Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization.
This option lets you set this seed yourself.
"""
range_start = 1
range_end = 9999999
default = "random"
@dataclass @dataclass
class TheWitnessOptions(PerGameCommonOptions): class TheWitnessOptions(PerGameCommonOptions):
puzzle_randomization: PuzzleRandomization puzzle_randomization: PuzzleRandomization
@@ -435,6 +445,7 @@ class TheWitnessOptions(PerGameCommonOptions):
laser_hints: LaserHints laser_hints: LaserHints
death_link: DeathLink death_link: DeathLink
death_link_amnesty: DeathLinkAmnesty death_link_amnesty: DeathLinkAmnesty
puzzle_randomization_seed: PuzzleRandomizationSeed
shuffle_dog: ShuffleDog shuffle_dog: ShuffleDog
@@ -445,7 +456,7 @@ witness_option_groups = [
MountainLasers, MountainLasers,
ChallengeLasers, ChallengeLasers,
]), ]),
OptionGroup("Panel Hunt Settings", [ OptionGroup("Panel Hunt Options", [
PanelHuntRequiredPercentage, PanelHuntRequiredPercentage,
PanelHuntTotal, PanelHuntTotal,
PanelHuntPostgame, PanelHuntPostgame,
@@ -483,6 +494,7 @@ witness_option_groups = [
ElevatorsComeToYou, ElevatorsComeToYou,
DeathLink, DeathLink,
DeathLinkAmnesty, DeathLinkAmnesty,
PuzzleRandomizationSeed,
]), ]),
OptionGroup("Silly Options", [ OptionGroup("Silly Options", [
ShuffleDog, ShuffleDog,

View File

@@ -2,7 +2,7 @@
Defines progression, junk and event items for The Witness Defines progression, junk and event items for The Witness
""" """
import copy import copy
from typing import TYPE_CHECKING, Dict, List, Set, cast from typing import TYPE_CHECKING, Dict, List, Set
from BaseClasses import Item, ItemClassification, MultiWorld from BaseClasses import Item, ItemClassification, MultiWorld

View File

@@ -233,6 +233,7 @@ class ZillionSkill(Range):
range_start = 0 range_start = 0
range_end = 5 range_end = 5
default = 2 default = 2
display_name = "skill"
class ZillionStartingCards(NamedRange): class ZillionStartingCards(NamedRange):