Compare commits

...

74 Commits

Author SHA1 Message Date
NewSoupVi
ead8efbc24 Fix using the wrong variable for slot lookup 2025-02-16 21:19:47 +01:00
NewSoupVi
aa1180e0aa Fix get_hint not checking for finding_player 2025-02-16 21:18:13 +01:00
qwint
34795b598a GER: Use Itempool Count for Minimal handling (#4649)
* uses itempool count vs unfilled location count instead of counting prog_items values which could have custom counters

* move unfilled location check to before can_reach

* add tests for successful minimal GER call with extra collect override prog_items in the pool to regression test issue fixed in this PR
2025-02-16 20:21:09 +01:00
JoshuaEagles
efd5004330 Docs: Update SA2B Linux and Steam Deck Setup Guide + Add Celeste 64 Linux Setup Guide (#4593)
* Update Linux and Steam Deck setup guide for sa2b

* Add Linux and Steam Deck setup guide for Celeste 64
2025-02-12 17:47:43 +01:00
Matthew Wells
c799531105 Docs: Add missing plural in faq (#4622) 2025-02-12 17:47:17 +01:00
threeandthreee
5c1ded1fe9 LADX: bomb as logical bush breaker #4636 2025-02-12 17:46:43 +01:00
qwint
b2162bb8e6 Docs: clean up create_item/event example (#4596)
* eyes

* remove line wraps where unnecessary
2025-02-12 17:46:07 +01:00
agilbert1412
f1769a8d00 Stardew Valley: Fixed Powdermelon and option inconsistencies (#4632)
* - Fixed powdermelon season

* - Improve cohesion in presets

* - Update several tooltips to be more consistent and accurate
2025-02-12 17:45:03 +01:00
qwint
f520c1d9f2 Launcher: Allow for --nogui client launches (#4549) 2025-02-10 19:34:27 +01:00
PinkSwitch
910369a7f8 Bizhawk Client: Display Err (#4532)
Co-authored-by: Bryce Wilson
2025-02-10 19:27:10 +01:00
qwint
dbf6b6f935 CC: don't try to reconnect on invalid version (#4606) 2025-02-10 19:23:58 +01:00
qwint
e9c463c897 CC: Force Text Client to always connect with empty game (#4607) 2025-02-10 19:23:09 +01:00
qwint
f4e43ca9e0 LttP: mock world.random in adjuster (#4623) 2025-02-10 19:22:06 +01:00
Fabian Dill
a298be9c41 Core: change HINT_FOUND to 40 and HINT_UNSPECIFIED to 0 (#4620) 2025-02-10 19:19:00 +01:00
Fabian Dill
18bcaa85a2 Test: ensure get_all_state() does not error in between steps (#4612) 2025-02-10 19:18:14 +01:00
Scipio Wright
359f45d50f TUNIC: Combat logic fix (#4589)
* Potential fix for attack issue

* also put the lazy version of the swamp fix in for good measure

* fix extra line

* now it is good

* Add the test, roll the other PR into this one

* Make the test exception more useful

* Remove debug print

* Combat logic fixed?

* Move a few areas to before well instead of east forest

* Put in qwint's suggestions in test

* Implement qwint's suggestions in combat_logic.py

* Implement qwint's suggestions for combat_logic.py

* Fix typo

* Remove experimental from combat logic description

* Remove copy_mixin again

* Add comment about copy_mixin

* Use a more proper random

* Some optimizations from Vi's comments
2025-02-09 19:12:17 +01:00
qwint
f5c574c37a Settings: add format handling to yaml exception marks for readability (#4531) 2025-02-09 12:11:27 +01:00
NewSoupVi
f75a1ae117 KH2: Fix lambda capture issue with weapon slot logic (#4604)
* KH2: Fix lambda capture issue with weapon slot logic

* Update Rules.py

* Improved by JaredWeakStrike (#4605)

* Apparently this wasn't meant to be indented

---------

Co-authored-by: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com>
2025-02-08 00:06:04 +01:00
Kory Dondzila
768ccffe72 Shivers: Update shivers links and guides (#4592) 2025-02-07 21:06:06 +01:00
Martmists
f6668997e6 [AHIT] Fix small options issue (#4615) 2025-02-07 21:02:37 +01:00
shananas
db11c620a7 KH2 Doc Update #4609
Mod Manager Version Number
2025-02-04 17:09:02 +01:00
Jouramie
da48af60dc Stardew Valley: add assert_can_reach_region_* for better tests (#4556)
* add assert_reach_region_*; refactor existing assert_reach_location_* to allow string

* rename asserts
2025-02-04 08:27:23 +01:00
massimilianodelliubaldini
19faaa4104 Core: Fix #4595 by using first type's docstring in a union type (#4600)
* Fix #4595: use first type's docstring in a union type.

* Reuse existing import.
2025-02-04 01:49:07 +01:00
Scipio Wright
628252896e TUNIC: Call Combat Logic experimental (#4594)
* Update options.py

* Update options.py
2025-02-03 15:53:56 +01:00
Mysteryem
f28aff6f9a Core: Replace generator creation/iteration in CollectionState methods (#4587)
* Core: Replace generator creation/iteration in CollectionState methods

Using generators in these functions incurs overhead to create the new
generator instance, call the `any`/`all`/`sum` function and have the
`any`/`all`/`sum` function iterate the generator, which in turn iterates
the iterable.

Replacing the use of generators with for loops is faster.

Getting `self.prog_items[player]` once in advance also improves
performance of iterating longer iterables.

* Add comment on the choice of for loops instead of any()/all()/sum()
2025-02-02 15:25:34 +01:00
Fabian Dill
894732be47 kvui: set home folder to non-default (#4590)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-02-02 02:53:16 +01:00
Jouramie
051518e72a Stardew Valley: Fix unresolved reference warning and unused imports (#4360)
* fix unresolved reference warning and unused imports

* revert stuff

* just a commit to rerun the tests cuz messenger fail
2025-02-01 22:07:08 +01:00
Spineraks
b7b78dead3 LADX: Fix generation error on minimal accessibility (#4281)
* [LADX] Fix minimal accessibility

* allow_partial for minimal accessibility

* create the correct partial_all_state

* skip our prefills rather than removing after

* dont rebuild our prefill list

---------

Co-authored-by: threeandthreee <a.l.nordstrom@gmail.com>
2025-02-01 22:03:49 +01:00
Jarno
d1167027f4 Core: Make csv options output ignore hidden options (#4539)
* Core: Make csv options output ignore hidden options

* Update Options.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-02-01 02:26:59 +01:00
qwint
445c9b22d6 Settings: Handle empty Groups (#4576)
* export empty groups as an empty dict instead of crashing

* Update settings.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* check instance values from self as well

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-02-01 02:11:04 +01:00
black-sliver
67e8877143 Docs: fix lower limit of valid IDs in network protocol.md (#4579) 2025-01-31 08:38:17 +01:00
agilbert1412
1fe8024b43 Stardew valley: Add Mod Recipes tests (#4580)
* `- Add Craftsanity Mod tests

* - Add the same test for cooking

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-01-30 09:19:06 +01:00
agilbert1412
8e14e463e4 Stardew Valley: Radioactive slot machine should be a ginger island check (#4578) 2025-01-30 09:05:51 +01:00
Jouramie
b8666b2562 Stardew Valley: Remove weird magic trap test? (#4570) 2025-01-29 13:56:50 -05:00
Felix R
57afdfda6f meritous: move completion_condition to set_rules (#4567) 2025-01-29 02:03:37 +01:00
black-sliver
738c21c625 Tests: massively improve the memory leak test performance (#4568)
* Tests: massively improve the memory leak test performance

With the growing number of worlds, GC becomes the bottleneck and slows down the test.

* Tests: fix typing in general/test_memory
2025-01-29 01:52:01 +01:00
black-sliver
41898ed640 MultiServer: implement NoText and deprecate uncompressed Websocket connections (#4540)
* MultiServer: add NoText tag and handling

* MultiServer: deprecate and warn for uncompressed connections

* MultiServer: fix missing space in no compression warning
2025-01-29 01:42:46 +01:00
agilbert1412
1ebc9e2ec0 Stardew Valley: Tests: Restructure the tests that validate Mods + ER together, improved performance (#4557)
* - Unrolled and improved the structure of the test for Mods + ER, to improve total performance and performance on individual tests for threading purposes

* Use | instead of Union[]

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>

* - Remove unused using

---------

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-01-28 23:19:20 +01:00
Silvris
9466d5274e MM2: fix plando and weakness special cases (#4561) 2025-01-28 21:45:28 +01:00
NewSoupVi
a53bcb4697 KH2: Use int(..., 0) in Client #4562 2025-01-27 23:13:10 +01:00
Exempt-Medic
8c5592e406 KH2: Fix determinism by using tuples instead of sets (#4548) 2025-01-27 11:06:10 -05:00
Bryce Wilson
41055cd963 Pokemon Emerald: Update changelog (#4551) 2025-01-27 17:01:18 +01:00
Scipio Wright
43874b1d28 Noita: Add clarification to check option descriptions (#4553) 2025-01-27 10:27:43 -05:00
Bryce Wilson
b570aa2ec6 Pokemon Emerald: Clean up free fly blacklist (#4552) 2025-01-27 10:25:31 -05:00
Bryce Wilson
c43233120a Pokemon Emerald: Clarify death link and start inventory descriptions (#4517) 2025-01-27 10:24:26 -05:00
Silvris
57a571cc11 KDL3: Fix world access on non-strict open world (#4543)
* Update rules.py

* lambda capture
2025-01-27 01:52:02 +01:00
Fabian Dill
8622cb6204 Factorio: Inventory Spill Traps (#4457) 2025-01-26 22:14:39 +01:00
qwint
90417e0022 CommonClient: Expand on make_gui docstring (#4449)
* adds docstring to make_gui describing what things you might want to change without dealing with kivy/kvui directly (there are better places to document those)

* Update CommonClient.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* Update CommonClient.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-26 13:06:27 +01:00
josephwhite
96b941ed35 Super Mario 64: Add Star Costs to Spoiler (#4544) 2025-01-25 09:36:23 -05:00
Bryce Wilson
1832bac1a3 BizHawkClient: Update README for get_memory_size (#4511) 2025-01-25 09:35:42 -05:00
qwint
86641223c1 Shivers: Stop using get_all_state cache to fix timing issue #4522
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-01-25 00:35:54 +01:00
black-sliver
cc770418f2 MultiServer: optimize PrintJSON for !release (#4545)
* MultiServer: optimize PrintJSON for !release

* MultiServer: safer comparison

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-24 23:22:33 +01:00
Scipio Wright
513e361764 TUNIC: Fix UT create_item classification (#4514)
Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
2025-01-24 17:10:58 -05:00
Silent
ddf7fdccc7 TUNIC: Add Torch Item (#4538)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-01-24 16:57:23 -05:00
Silent
3df2dbe051 TUNIC: Add ability shuffle information to spoiler log (#4498) 2025-01-24 16:55:49 -05:00
Jasper den Brok
3d1d6908c8 Pokemon Emerald: Add Free Fly Blacklist (#4165)
Co-authored-by: Jasper den Brok <jasper.den.brok@gmail.com>
2025-01-24 16:30:21 -05:00
qwint
7474c27372 Core: Add launch function to call launch_subprocess only if multiprocessing is actually necessary (#4237)
* skips opening a subprocess if kivy (and thus the launcher gui) hasn't been loaded so stdin can function as expected on --nogui and similar

* this exists lol

* keep old function around and use new function for CC component

* fix name=None typing
2025-01-24 19:52:12 +01:00
Scipio Wright
bb0948154d TUNIC: Make the standard entrances get made with tuples instead of sets (#4546)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-24 12:42:31 -05:00
CookieCat
fa2816822b AHIT: Fix broken link in setup guide (#4524)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-23 16:45:11 -05:00
NewSoupVi
5a42c70675 Core: Fix worlds that rely on other worlds having their Entrances connected before connect_entrances, add unit test (#4530)
* unit test that get all state is called with partial entrances before connect_entrances

* fix the two worlds doing it

* lol

* unused import

* Update test/general/test_entrances.py

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

* Update test_entrances.py

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-22 14:00:47 +01:00
JaredWeakStrike
949527f9cb KH2: Bug fixes and game update future proofing (#4075)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-01-21 17:28:33 -05:00
Scipio Wright
1a1b7e9cf4 TUNIC: Reduce range end for local_fill option #4534 2025-01-21 18:39:08 +01:00
Fabian Dill
edacb17171 Factorio: remove debug print (#4533) 2025-01-21 16:12:53 +01:00
qwint
33fd9de281 Core: Add Retry to Priority Fill (#4477)
* adds a retry to priority fill in case the one item per player optimization would cause the priority fill to fail to find valid placements

* Update Fill.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>
2025-01-21 00:56:20 +01:00
qwint
a126dee068 HK: some stuff ruff and pycodestyle complained about (#4523) 2025-01-20 23:42:12 +01:00
qwint
e2b942139a HK: Save GrubHuntGoal by value (#4521) 2025-01-20 19:10:29 +01:00
Scipio Wright
823b17c386 TUNIC: Make grass go in the regular location name group too (#4504)
* Make grass go in the normal loc group too

* Make it not overwrite old groups
2025-01-20 17:44:39 +01:00
Chris J.
05d1b2129a Docs: Update ID Overlapping Docs (#4447) 2025-01-20 11:18:09 -05:00
NewSoupVi
436c0a4104 Core: Add connect_entrances world step/stage (#4420)
* Add connect_entrances

* update ER docs

* fix that test, but also ew

* Add a test that asserts the new finalization

* Rewrite test a bit

* rewrite some more

* blank line

* rewrite rewrite rewrite

* rewrite rewrite rewrite

* RE. WRITE.

* oops

* Bruh

* I guess, while we're at it

* giga oops

* It's been a long day

* Switch KH1 over to this design with permission of GICU

* Revert

* Oops

* Bc I like it

* Update locations.py
2025-01-20 16:07:15 +01:00
Scipio Wright
96f469c737 TUNIC: Fix hero relics not being prog if hex quest is on in combat logic #4509 2025-01-20 16:04:39 +01:00
Scipio Wright
4f77abac4f TUNIC: Fix failure in 1-player grass (#4520)
* Fix failure in 1-player grass

* Update worlds/tunic/__init__.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>
2025-01-20 15:53:30 +01:00
massimilianodelliubaldini
d5cd95c7fb Docs: Clarify usage of slot data for trackers in World API doc (#3986)
* Clarify usage of slot data for trackers in world API.

* Typo.

* Update docs/world api.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Update docs/world api.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Update docs/world api.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Update docs/world api.md

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

* Keep to 120 char lines.

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-20 09:01:45 +01:00
Exempt-Medic
a2fbf856ff SMZ3: Change locality options earlier (#4424) 2025-01-19 23:07:01 -05:00
Exempt-Medic
4fa8c43266 FFMQ: Fix collect_item (#4433)
* Fix FFMQ collect_item
2025-01-19 23:06:09 -05:00
106 changed files with 1794 additions and 1053 deletions

View File

@@ -869,21 +869,40 @@ class CollectionState():
def has(self, item: str, player: int, count: int = 1) -> bool:
return self.prog_items[player][item] >= count
# for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of
# creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the
# argument to all() would be a new generator instance, for example.
def has_all(self, items: Iterable[str], player: int) -> bool:
"""Returns True if each item name of items is in state at least once."""
return all(self.prog_items[player][item] for item in items)
player_prog_items = self.prog_items[player]
for item in items:
if not player_prog_items[item]:
return False
return True
def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)
player_prog_items = self.prog_items[player]
for item in items:
if player_prog_items[item]:
return True
return False
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
player_prog_items = self.prog_items[player]
for item, count in item_counts.items():
if player_prog_items[item] < count:
return False
return True
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
player_prog_items = self.prog_items[player]
for item, count in item_counts.items():
if player_prog_items[item] >= count:
return True
return False
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
@@ -911,11 +930,20 @@ class CollectionState():
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)
player_prog_items = self.prog_items[player]
total = 0
for item_name in items:
total += player_prog_items[item_name]
return total
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
player_prog_items = self.prog_items[player]
total = 0
for item_name in items:
if player_prog_items[item_name] > 0:
total += 1
return total
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:

View File

@@ -709,8 +709,16 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def make_gui(self) -> typing.Type["kvui.GameManager"]:
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
def make_gui(self) -> "type[kvui.GameManager]":
"""
To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built
Common changes are changing `base_title` to update the window title of the client and
updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger.
ex. `logging_pairs.append(("Foo", "Bar"))`
will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")`
"""
from kvui import GameManager
class TextManager(GameManager):
@@ -899,6 +907,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.disconnected_intentionally = True
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
ctx.disconnected_intentionally = True
raise Exception('Server reported your client version as incompatible. '
'This probably means you have to update.')
elif 'InvalidItemsHandling' in errors:
@@ -1087,7 +1096,7 @@ def run_as_textclient(*args):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
await self.send_connect(game="")
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":

View File

@@ -502,7 +502,13 @@ def distribute_items_restrictive(multiworld: MultiWorld,
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=False)
name="Priority", one_item_per_player=True, allow_partial=True)
if prioritylocations:
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False)
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

View File

@@ -33,10 +33,15 @@ WINDOW_MIN_HEIGHT = 525
WINDOW_MIN_WIDTH = 425
class AdjusterWorld(object):
class AdjusterSubWorld(object):
def __init__(self, random):
self.random = random
def __init__(self, sprite_pool):
import random
self.sprite_pool = {1: sprite_pool}
self.per_slot_randoms = {1: random}
self.worlds = {1: self.AdjusterSubWorld(random)}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):

View File

@@ -148,7 +148,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
multiworld.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set()
AutoWorld.call_all(multiworld, "connect_entrances")
AutoWorld.call_all(multiworld, "generate_basic")
# remove starting inventory from pool items.

View File

@@ -28,9 +28,11 @@ ModuleUpdate.update()
if typing.TYPE_CHECKING:
import ssl
from NetUtils import ServerConnection
import websockets
import colorama
import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate
try:
# ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError
@@ -119,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint):
version = Version(0, 0, 0)
tags: typing.List[str] = []
tags: typing.List[str]
remote_items: bool
remote_start_inventory: bool
no_items: bool
no_locations: bool
no_text: bool
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
super().__init__(socket)
self.auth = False
self.team = None
@@ -175,6 +178,7 @@ class Context:
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
endpoints: list[Client]
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
hints_used: typing.Dict[typing.Tuple[int, int], int]
@@ -364,18 +368,28 @@ class Context:
return True
def broadcast_all(self, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
data = self.dumper(msgs)
endpoints = (
endpoint
for endpoint in self.endpoints
if endpoint.auth and not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
self.logger.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
data = self.dumper(msgs)
endpoints = (
endpoint
for endpoint in itertools.chain.from_iterable(self.clients[team].values())
if not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
msgs = self.dumper(msgs)
@@ -389,13 +403,13 @@ class Context:
await on_client_disconnected(self, endpoint)
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
if not client.auth:
if not client.auth or client.no_text:
return
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
if not client.auth:
if not client.auth or client.no_text:
return
async_start(self.send_msgs(client,
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
@@ -760,7 +774,7 @@ class Context:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
if recipients is None or slot in recipients:
clients = self.clients[team].get(slot)
clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, []))
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
@@ -769,7 +783,7 @@ class Context:
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
for hint in self.hints[team, finding_player]:
if hint.location == seeked_location:
if hint.location == seeked_location and hint.finding_player == finding_player:
return hint
return None
@@ -819,7 +833,7 @@ def update_aliases(ctx: Context, team: int):
async_start(ctx.send_encoded_msgs(client, cmd))
async def server(websocket, path: str = "/", ctx: Context = None):
async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None:
client = Client(websocket, ctx)
ctx.endpoints.append(client)
@@ -910,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client):
"If your client supports it, "
"you may have additional local commands you can list with /help.",
{"type": "Tutorial"})
if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions):
ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! "
"It may stop working in the future. If you are a player, please report this to the "
"client's developer.")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -1060,21 +1078,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
count_activity: bool = True):
slot_locations = ctx.locations[slot]
new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata
if new_locations:
if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
sortable: list[tuple[int, int, int, int]] = []
for location in new_locations:
item_id, target_player, flags = ctx.locations[slot][location]
# extract all fields to avoid runtime overhead in LocationStore
item_id, target_player, flags = slot_locations[location]
# sort/group by receiver and item
sortable.append((target_player, item_id, location, flags))
info_texts: list[dict[str, typing.Any]] = []
for target_player, item_id, location, flags in sorted(sortable):
new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item)
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
if len(info_texts) >= 140:
# split into chunks that are close to compression window of 64K but not too big on the wire
# (roughly 1300-2600 bytes after compression depending on repetitiveness)
ctx.broadcast_team(team, info_texts)
info_texts.clear()
info_texts.append(json_format_send_event(new_item, target_player))
ctx.broadcast_team(team, info_texts)
del info_texts
del sortable
ctx.location_checks[team, slot] |= new_locations
send_new_items(ctx)
@@ -1101,7 +1135,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id):
prev_hint = ctx.get_hint(team, slot, location_id)
prev_hint = ctx.get_hint(team, finding_player, location_id)
if prev_hint:
hints.append(prev_hint)
else:
@@ -1787,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.clients[team][slot].append(client)
client.version = args['version']
client.tags = args['tags']
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags
# set NoText for old PopTracker clients that predate the tag to save traffic
client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1))
connected_packet = {
"cmd": "Connected",
"team": client.team, "slot": client.slot,
@@ -1860,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.tags = args["tags"]
if set(old_tags) != set(client.tags):
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_text = "NoText" in client.tags or (
"PopTracker" in client.tags and client.version < (0, 5, 1)
)
ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.",

View File

@@ -5,17 +5,18 @@ import enum
import warnings
from json import JSONEncoder, JSONDecoder
import websockets
if typing.TYPE_CHECKING:
from websockets import WebSocketServerProtocol as ServerConnection
from Utils import ByValue, Version
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_UNSPECIFIED = 0
HINT_NO_PRIORITY = 10
HINT_AVOID = 20
HINT_PRIORITY = 30
HINT_FOUND = 40
class JSONMessagePart(typing.TypedDict, total=False):
@@ -151,7 +152,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint:
socket: websockets.WebSocketServerProtocol
socket: "ServerConnection"
def __init__(self, socket):
self.socket = socket

View File

@@ -1582,7 +1582,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
if issubclass(Removed, option):
if option.visibility == Visibility.none:
continue
display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name

View File

@@ -22,7 +22,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
Here is a list of our [Supported Games](https://archipelago.gg/games).
## Can I generate a single-player game with Archipelago?

View File

@@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups)
#### When to call `randomize_entrances`
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
The correct step for this is `World.connect_entrances`.
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
This means 2 things about when you can call ER:
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
and create your events before you call ER if you want to guarantee a correct output.
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
well.
Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`.
However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better
together.
These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`.
It is fine for your Entrances to be connected differently or not at all before this step.
#### Informing your client about randomized entrances

View File

@@ -47,6 +47,9 @@ Packets are simple JSON lists in which any number of ordered network commands ca
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
Websocket connections should support per-message compression. Uncompressed connections are deprecated and may stop
working in the future.
Example:
```javascript
[{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }]
@@ -360,11 +363,11 @@ An enumeration containing the possible hint states.
```python
import enum
class HintStatus(enum.IntEnum):
HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
HINT_UNSPECIFIED = 0 # The receiving player has not specified any status
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
HINT_FOUND = 40 # The location has been collected. Status cannot be changed once found.
```
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
@@ -530,9 +533,9 @@ In JSON this may look like:
{"item": 3, "location": 3, "player": 3, "flags": 0}
]
```
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
@@ -745,6 +748,7 @@ Tags are represented as a list of strings, the common client tags follow:
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| NoText | Indicates the client does not want to receive text messages, improving performance if not needed. |
¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\
²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped.

View File

@@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
Locations and items can share IDs, so typically a game's locations and items start at the same ID.
letter or symbol). The ID needs to be unique across all locations within the game.
Locations and items can share IDs, and locations can share IDs with other games' locations.
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
@@ -243,7 +243,9 @@ progression. Progression items will be assigned to locations with higher priorit
and satisfy progression balancing.
The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
The ID thus also needs to be unique across all items with different names within the game.
Items and locations can share IDs, and items can share IDs with other games' items.
Other classifications include:
@@ -490,6 +492,9 @@ In addition, the following methods can be implemented and are called in this ord
after this step. Locations cannot be moved to different regions after this step.
* `set_rules(self)`
called to set access and item rules on locations and entrances.
* `connect_entrances(self)`
by the end of this step, all entrances must exist and be connected to their source and target regions.
Entrance randomization should be done here.
* `generate_basic(self)`
player-specific randomization that does not affect logic can be done here.
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
@@ -557,17 +562,13 @@ from .items import is_progression # this is just a dummy
def create_item(self, item: str) -> MyGameItem:
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
classification = ItemClassification.progression if is_progression(item) else
ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item],
self.player)
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
def create_event(self, event: str) -> MyGameItem:
# while we are at it, we can also add a helper to create events
return MyGameItem(event, True, None, self.player)
return MyGameItem(event, ItemClassification.progression, None, self.player)
```
#### create_items
@@ -835,14 +836,16 @@ def generate_output(self, output_directory: str) -> None:
### Slot Data
If the game client needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
a `dict` with `str` keys that can be serialized with json.
But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
once it has successfully [connected](network%20protocol.md#connected).
If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with
`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is
absolutely necessary. Slot data is sent to your client once it has successfully
[connected](network%20protocol.md#connected).
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
common usage of slot data is sending option results that the client needs to be aware of.
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding
item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
```python
def fill_slot_data(self) -> Dict[str, Any]:

View File

@@ -378,13 +378,14 @@ def randomize_entrances(
and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
# ensure that we have enough locations to place our progression
accessible_location_count = 0
prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
prog_item_count = len([item for item in world.multiworld.itempool if item.advancement and item.player == world.player])
# short-circuit location checking in this case
if prog_item_count == 0:
return True
for region in er_state.placed_regions:
for loc in region.locations:
if loc.can_reach(er_state.collection_state):
if not loc.item and loc.can_reach(er_state.collection_state):
# don't count locations with preplaced items
accessible_location_count += 1
if accessible_location_count >= prog_item_count:
perform_validity_check = False

19
kvui.py
View File

@@ -26,6 +26,10 @@ import Utils
if Utils.is_frozen():
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
import platformdirs
os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy")
os.makedirs(os.environ["KIVY_HOME"], exist_ok=True)
from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
@@ -440,8 +444,11 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if child.collide_point(*touch.pos):
key = child.sort_key
if key == "status":
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]]
else:
parent.hint_sorter = lambda element: (
remove_between_brackets.sub("", element[key]["text"]).lower()
)
if key == parent.sort_key:
# second click reverses order
parent.reversed = not parent.reversed
@@ -825,7 +832,13 @@ status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum",
}
status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_FOUND: 0,
HintStatus.HINT_UNSPECIFIED: 1,
HintStatus.HINT_NO_PRIORITY: 2,
HintStatus.HINT_AVOID: 3,
HintStatus.HINT_PRIORITY: 4,
}
class HintLog(RecycleView):

View File

@@ -109,7 +109,7 @@ class Group:
def get_type_hints(cls) -> Dict[str, Any]:
"""Returns resolved type hints for the class"""
if cls._type_cache is None:
if not isinstance(next(iter(cls.__annotations__.values())), str):
if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str):
# non-str: assume already resolved
cls._type_cache = cls.__annotations__
else:
@@ -270,15 +270,20 @@ class Group:
# fetch class to avoid going through getattr
cls = self.__class__
type_hints = cls.get_type_hints()
entries = [e for e in self]
if not entries:
# write empty dict for empty Group with no instance values
cls._dump_value({}, f, indent=" " * level)
# validate group
for name in cls.__annotations__.keys():
assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
# dump ordered members
for name in self:
for name in entries:
attr = cast(object, getattr(self, name))
attr_cls = type_hints[name] if name in type_hints else attr.__class__
attr_cls_origin = typing.get_origin(attr_cls)
while attr_cls_origin is Union: # resolve to first type for doc string
# resolve to first type for doc string
while attr_cls_origin is Union or attr_cls_origin is types.UnionType:
attr_cls = typing.get_args(attr_cls)[0]
attr_cls_origin = typing.get_origin(attr_cls)
if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
@@ -787,7 +792,17 @@ class Settings(Group):
if location:
from Utils import parse_yaml
with open(location, encoding="utf-8-sig") as f:
options = parse_yaml(f.read())
from yaml.error import MarkedYAMLError
try:
options = parse_yaml(f.read())
except MarkedYAMLError as ex:
if ex.problem_mark:
f.seek(0)
lines = f.readlines()
problem_line = lines[ex.problem_mark.line]
error_line = " " * ex.problem_mark.column + "^"
raise Exception(f"{ex.context} {ex.problem}\n{problem_line}{error_line}")
raise ex
# TODO: detect if upgrade is required
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
self.update(options or {})

View File

@@ -18,7 +18,15 @@ def run_locations_benchmark():
class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = (
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
rule_iterations: int = 100_000
if sys.version_info >= (3, 9):

View File

@@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul
from worlds import network_data_package
from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
gen_steps = (
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def setup_solo_multiworld(

View File

@@ -311,6 +311,37 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_minimal_entrance_rando_with_collect_override(self):
"""
tests that entrance randomization can complete with minimal accessibility and unreachable exits
when the world defines a collect override that add extra values to prog_items
"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(10, 1, True)
multiworld.itempool += prog_items
filler_items = generate_items(15, 1, False)
multiworld.itempool += filler_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
old_collect = multiworld.worlds[1].collect
def new_collect(state, item):
old_collect(state, item)
state.prog_items[item.player]["counter"] += 300
multiworld.worlds[1].collect = new_collect
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_restrictive_region_requirement_does_not_fail(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 2, 1)

View File

@@ -0,0 +1,63 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all, World
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
def test_entrance_connection_steps(self):
"""Tests that Entrances are connected and not changed after connect_entrances."""
def get_entrance_name_to_source_and_target_dict(world: World):
return [
(entrance.name, entrance.parent_region, entrance.connected_region)
for entrance in world.get_entrances()
]
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
additional_steps = ("generate_basic", "pre_fill")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertTrue(
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
f"{game_name} had unconnected entrances after connect_entrances"
)
for step in additional_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertEqual(
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
)
def test_all_state_before_connect_entrances(self):
"""Before connect_entrances, Entrance objects may be unconnected.
Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during
connect_entrances."""
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, ())
original_get_all_state = multiworld.get_all_state
def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))
return original_get_all_state(use_cache, allow_partial_entrances)
multiworld.get_all_state = patched_get_all_state
for step in gen_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)

View File

@@ -67,7 +67,7 @@ class TestBase(unittest.TestCase):
def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
worlds_to_test = {game: world
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
@@ -84,7 +84,7 @@ class TestBase(unittest.TestCase):
def test_locality_not_modified(self):
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):

View File

@@ -45,6 +45,12 @@ class TestBase(unittest.TestCase):
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "connect_entrances")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during rule creation")
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic")

View File

@@ -1,5 +1,6 @@
import unittest
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
@@ -9,8 +10,12 @@ class TestWorldMemory(unittest.TestCase):
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
import gc
import weakref
refs: dict[str, weakref.ReferenceType[MultiWorld]] = {}
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
with self.subTest("Game creation", game_name=game_name):
weak = weakref.ref(setup_solo_multiworld(world_type))
gc.collect()
refs[game_name] = weak
gc.collect()
for game_name, weak in refs.items():
with self.subTest("Game cleanup", game_name=game_name):
self.assertFalse(weak(), "World leaked a reference")

View File

@@ -2,11 +2,11 @@ import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
from . import setup_solo_multiworld, gen_steps
class TestBase(unittest.TestCase):
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
gen_steps = gen_steps
default_settings_unreachable_regions = {
"A Link to the Past": {

View File

@@ -0,0 +1,29 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
gen_steps = (
"generate_early",
"create_regions",
)
test_steps = (
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def test_all_state_is_available(self):
"""Ensure all_state can be created at certain steps."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
for step in self.test_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
self.assertTrue(multiworld.get_all_state(False, True))

View File

@@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister):
"""Method for setting the rules on the World's regions and locations."""
pass
def connect_entrances(self) -> None:
"""Method to finalize the source and target regions of the World's entrances"""
pass
def generate_basic(self) -> None:
"""
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.

View File

@@ -87,7 +87,7 @@ class Component:
processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args)
@@ -95,6 +95,14 @@ def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] =
processes.add(process)
def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
from Utils import is_kivy_running
if is_kivy_running():
launch_subprocess(func, name, args)
else:
func(*args)
class SuffixIdentifier:
suffixes: Iterable[str]
@@ -111,7 +119,7 @@ class SuffixIdentifier:
def launch_textclient(*args):
import CommonClient
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
launch(CommonClient.run_as_textclient, name="TextClient", args=args)
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:

View File

@@ -55,6 +55,7 @@ async def lock(ctx) -> None
async def unlock(ctx) -> None
async def get_hash(ctx) -> str
async def get_memory_size(ctx, domain: str) -> int
async def get_system(ctx) -> str
async def get_cores(ctx) -> dict[str, str]
async def ping(ctx) -> None
@@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe
associate the file extension with Archipelago.
`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
ROM as yours, this is where you should do setup for things like `items_handling`.
running on a system you specified in your `system` class variable. Take extra care here, because your code will run
against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size
of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where
you should do setup for things like `items_handling`.
`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
@@ -268,6 +270,8 @@ server connection before trying to interact with it.
- By default, the player will be asked to provide their slot name after connecting to the server and validating, and
that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
set it automatically based on data in the ROM or on your client instance.
- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a
smaller ROM size.
- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
subclass of `CommonContext` and its API.
- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
import abc
from typing import TYPE_CHECKING, Any, ClassVar
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch as launch_component
if TYPE_CHECKING:
from .context import BizHawkClientContext
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def launch_client(*args) -> None:
from .context import launch
launch_subprocess(launch, name="BizHawkClient", args=args)
launch_component(launch, name="BizHawkClient", args=args)
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,

View File

@@ -238,6 +238,7 @@ def _patch_and_run_game(patch_file: str):
return metadata
except Exception as exc:
logger.exception(exc)
Utils.messagebox("Error Patching Game", str(exc), True)
return {}

View File

@@ -338,7 +338,7 @@ class MinExtraYarn(Range):
There must be at least this much more yarn over the total number of yarn needed to craft all hats.
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
there must be at least 50 yarn in the pool."""
display_name = "Max Extra Yarn"
display_name = "Min Extra Yarn"
range_start = 5
range_end = 15
default = 10

View File

@@ -12,13 +12,13 @@ from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
from worlds.AutoWorld import World, WebWorld, CollectionState
from worlds.generic.Rules import add_rule
from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
from worlds.LauncherComponents import Component, components, icon_paths, launch as launch_component, Type
from Utils import local_path
def launch_client():
from .Client import launch
launch_subprocess(launch, name="AHITClient")
launch_component(launch, name="AHITClient")
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,

View File

@@ -21,7 +21,7 @@
3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601).
4. Once the game finishes downloading, start it up.
@@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked.
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
if you have too many save files. Delete them and it should fix the problem.
if you have too many save files. Delete them and it should fix the problem.

View File

@@ -1125,7 +1125,7 @@ def set_trock_key_rules(world, player):
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False)
all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True)
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True

View File

@@ -12,6 +12,12 @@
1. Download the above release and extract it.
## Installation Procedures (Linux and Steam Deck)
1. Download the above release and extract it.
2. Add Celeste64.exe to Steam as a Non-Steam Game. In the properties for it on Steam, set it to use Proton as the compatibility tool. Launch the game through Steam in order to run it.
## Joining a MultiWorld Game
1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install.
@@ -33,5 +39,3 @@ An Example `AP.json` file:
"Password": ""
}
```

View File

@@ -304,6 +304,11 @@ class EvolutionTrapIncrease(Range):
range_end = 100
class InventorySpillTrapCount(TrapCount):
"""Trap items that when received trigger dropping your main inventory and trash inventory onto the ground."""
display_name = "Inventory Spill Traps"
class FactorioWorldGen(OptionDict):
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
@@ -484,6 +489,7 @@ class FactorioOptions(PerGameCommonOptions):
artillery_traps: ArtilleryTrapCount
atomic_rocket_traps: AtomicRocketTrapCount
atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount
inventory_spill_traps: InventorySpillTrapCount
attack_traps: AttackTrapCount
evolution_traps: EvolutionTrapCount
evolution_trap_increase: EvolutionTrapIncrease
@@ -518,6 +524,7 @@ option_groups: list[OptionGroup] = [
ArtilleryTrapCount,
AtomicRocketTrapCount,
AtomicCliffRemoverTrapCount,
InventorySpillTrapCount,
],
start_collapsed=True
),

View File

@@ -8,7 +8,7 @@ import Utils
import settings
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
from worlds.generic import Rules
from .Locations import location_pools, location_table
from .Mod import generate_mod
@@ -24,7 +24,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
def launch_client():
from .Client import launch
launch_subprocess(launch, name="FactorioClient")
launch_component(launch, name="FactorioClient")
components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT))
@@ -78,6 +78,7 @@ all_items["Cluster Grenade Trap"] = factorio_base_id - 5
all_items["Artillery Trap"] = factorio_base_id - 6
all_items["Atomic Rocket Trap"] = factorio_base_id - 7
all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8
all_items["Inventory Spill Trap"] = factorio_base_id - 9
class Factorio(World):
@@ -112,6 +113,8 @@ class Factorio(World):
science_locations: typing.List[FactorioScienceLocation]
removed_technologies: typing.Set[str]
settings: typing.ClassVar[FactorioSettings]
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player)
@@ -136,15 +139,11 @@ class Factorio(World):
random = self.random
nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.options.evolution_traps + \
self.options.attack_traps + \
self.options.teleport_traps + \
self.options.grenade_traps + \
self.options.cluster_grenade_traps + \
self.options.atomic_rocket_traps + \
self.options.atomic_cliff_remover_traps + \
self.options.artillery_traps
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo
for name in self.trap_names:
name = name.replace(" ", "_").lower()+"_traps"
location_count += getattr(self.options, name)
location_pool = []
@@ -196,9 +195,8 @@ class Factorio(World):
def create_items(self) -> None:
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket",
"Atomic Cliff Remover")
for trap_name in traps:
for trap_name in self.trap_names:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.options,
f"{trap_name.lower().replace(' ', '_')}_traps")))
@@ -280,9 +278,6 @@ class Factorio(World):
self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
for tech_name in victory_tech_names:
if not self.multiworld.get_all_state(True).has(tech_name, player):
print(tech_name)
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
def get_recipe(self, name: str) -> Recipe:

View File

@@ -48,3 +48,40 @@ function fire_entity_at_entities(entity_name, entities, speed)
target=target, speed=speed}
end
end
function spill_character_inventory(character)
if not (character and character.valid) then
return false
end
-- grab attrs once pre-loop
local position = character.position
local surface = character.surface
local inventories_to_spill = {
defines.inventory.character_main, -- Main inventory
defines.inventory.character_trash, -- Logistic trash slots
}
for _, inventory_type in pairs(inventories_to_spill) do
local inventory = character.get_inventory(inventory_type)
if inventory and inventory.valid then
-- Spill each item stack onto the ground
for i = 1, #inventory do
local stack = inventory[i]
if stack and stack.valid_for_read then
local spilled_items = surface.spill_item_stack{
position = position,
stack = stack,
enable_looted = false, -- do not mark for auto-pickup
force = nil, -- do not mark for auto-deconstruction
allow_belts = true, -- do mark for putting it onto belts
}
if #spilled_items > 0 then
stack.clear() -- only delete if spilled successfully
end
end
end
end
end
end

View File

@@ -750,6 +750,11 @@ end,
fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1)
end
end,
["Inventory Spill Trap"] = function ()
for _, player in ipairs(game.forces["player"].players) do
spill_character_inventory(player.character)
end
end,
}
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)

View File

@@ -152,14 +152,23 @@ class FFMQWorld(World):
return FFMQItem(name, self.player)
def collect_item(self, state, item, remove=False):
if not item.advancement:
return None
if "Progressive" in item.name:
i = item.code - 256
if remove:
if state.has(self.item_id_to_name[i+1], self.player):
if state.has(self.item_id_to_name[i+2], self.player):
return self.item_id_to_name[i+2]
return self.item_id_to_name[i+1]
return self.item_id_to_name[i]
if state.has(self.item_id_to_name[i], self.player):
if state.has(self.item_id_to_name[i+1], self.player):
return self.item_id_to_name[i+2]
return self.item_id_to_name[i+1]
return self.item_id_to_name[i]
return item.name if item.advancement else None
return item.name
def modify_multidata(self, multidata):
# wait for self.rom_name to be available.

View File

@@ -333,7 +333,7 @@ class PlandoCharmCosts(OptionDict):
continue
try:
self.value[key] = CharmCost.from_any(data).value
except ValueError as ex:
except ValueError:
# will fail schema afterwords
self.value[key] = data

View File

@@ -7,22 +7,22 @@ import itertools
import operator
from collections import defaultdict, Counter
logger = logging.getLogger("Hollow Knight")
from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions
from .Items import item_table, item_name_groups
from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
shop_to_option, HKOptions, GrubHuntGoal
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .ExtractedData import locations, starts, multi_locations, event_names, item_effects, connectors, \
vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, \
CollectionState
from worlds.AutoWorld import World, LogicMixin, WebWorld
from settings import Group, Bool
logger = logging.getLogger("Hollow Knight")
class HollowKnightSettings(Group):
class DisableMapModSpoilers(Bool):
@@ -160,7 +160,7 @@ class HKWeb(WebWorld):
class HKWorld(World):
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
searching for riches, or glory, or answers to old secrets.
As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils.
@@ -209,7 +209,7 @@ class HKWorld(World):
# defaulting so completion condition isn't incorrect before pre_fill
self.grub_count = (
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
else options.GrubHuntGoal
else options.GrubHuntGoal.value
)
self.grub_player_count = {self.player: self.grub_count}
@@ -231,7 +231,6 @@ class HKWorld(World):
def create_regions(self):
menu_region: Region = create_region(self.multiworld, self.player, 'Menu')
self.multiworld.regions.append(menu_region)
# wp_exclusions = self.white_palace_exclusions()
# check for any goal that godhome events are relevant to
all_event_names = event_names.copy()
@@ -241,21 +240,17 @@ class HKWorld(World):
# Link regions
for event_name in sorted(all_event_names):
#if event_name in wp_exclusions:
# continue
loc = HKLocation(self.player, event_name, None, menu_region)
loc.place_locked_item(HKItem(event_name,
True, #event_name not in wp_exclusions,
True,
None, "Event", self.player))
menu_region.locations.append(loc)
for entry_transition, exit_transition in connectors.items():
#if entry_transition in wp_exclusions:
# continue
if exit_transition:
# if door logic fulfilled -> award vanilla target as event
loc = HKLocation(self.player, entry_transition, None, menu_region)
loc.place_locked_item(HKItem(exit_transition,
True, #exit_transition not in wp_exclusions,
True,
None, "Event", self.player))
menu_region.locations.append(loc)
@@ -292,7 +287,10 @@ class HKWorld(World):
if item_name in junk_replace:
item_name = self.get_filler_item_name()
item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
item = (self.create_item(item_name)
if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations
else self.create_event(item_name)
)
if location_name == "Start":
if item_name in randomized_starting_items:
@@ -347,8 +345,8 @@ class HKWorld(World):
randomized = True
_add("Elevator_Pass", "Elevator_Pass", randomized)
for shop, locations in self.created_multi_locations.items():
for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
for shop, shop_locations in self.created_multi_locations.items():
for _ in range(len(shop_locations), getattr(self.options, shop_to_option[shop]).value):
self.create_location(shop)
unfilled_locations += 1
@@ -358,7 +356,7 @@ class HKWorld(World):
# Add additional shop items, as needed.
if additional_shop_items > 0:
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
shops = [shop for shop, shop_locations in self.created_multi_locations.items() if len(shop_locations) < 16]
if not self.options.EggShopSlots: # No eggshop, so don't place items there
shops.remove('Egg_Shop')
@@ -380,8 +378,8 @@ class HKWorld(World):
self.sort_shops_by_cost()
def sort_shops_by_cost(self):
for shop, locations in self.created_multi_locations.items():
randomized_locations = list(loc for loc in locations if not loc.vanilla)
for shop, shop_locations in self.created_multi_locations.items():
randomized_locations = [loc for loc in shop_locations if not loc.vanilla]
prices = sorted(
(loc.costs for loc in randomized_locations),
key=lambda costs: (len(costs),) + tuple(costs.values())
@@ -405,7 +403,7 @@ class HKWorld(World):
return {k: v for k, v in weights.items() if v}
random = self.random
hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
hybrid_chance = getattr(self.options, "CostSanityHybridChance").value
weights = {
data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
for data in cost_terms.values()
@@ -493,7 +491,11 @@ class HKWorld(World):
worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]]
if worlds:
grubs = [item for item in multiworld.get_items() if item.name == "Grub"]
all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]]
all_grub_players = [
world.player
for world in worlds
if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
]
if all_grub_players:
group_lookup = defaultdict(set)
@@ -668,8 +670,8 @@ class HKWorld(World):
):
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
else:
for shop_name, locations in hk_world.created_multi_locations.items():
for loc in locations:
for shop_name, shop_locations in hk_world.created_multi_locations.items():
for loc in shop_locations:
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:

View File

@@ -2,7 +2,6 @@ import typing
from argparse import Namespace
from BaseClasses import CollectionState, MultiWorld
from Options import ItemLinks
from test.bases import WorldTestBase
from worlds.AutoWorld import AutoWorldRegister, call_all
from .. import HKWorld

View File

@@ -1,5 +1,6 @@
from . import linkedTestHK, WorldTestBase
from test.bases import WorldTestBase
from Options import ItemLinks
from . import linkedTestHK
class test_grubcount_limited(linkedTestHK, WorldTestBase):

View File

@@ -206,19 +206,19 @@ def set_rules(world: "KDL3World") -> None:
lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player),
lambda state: can_reach_cutter(state, world.player))
@@ -248,9 +248,9 @@ def set_rules(world: "KDL3World") -> None:
for i in range(12, 18):
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
for i in range(21, 23):
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
lambda state: can_reach_chuchu(state, world.player))
@@ -307,7 +307,7 @@ def set_rules(world: "KDL3World") -> None:
lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player))
set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player),
lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player)
and can_reach_burning(state, world.player))
and can_reach_burning(state, world.player))
for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
"Level 3 Boss - Purified", "Level 4 Boss - Purified",
@@ -329,6 +329,14 @@ def set_rules(world: "KDL3World") -> None:
world.options.ow_boss_requirement.value,
world.player_levels)))
if world.options.open_world:
for boss_flag, level in zip(["Level 1 Boss - Defeated", "Level 2 Boss - Defeated", "Level 3 Boss - Defeated",
"Level 4 Boss - Defeated", "Level 5 Boss - Defeated"],
location_name.level_names.keys()):
set_rule(world.get_location(boss_flag),
lambda state, lvl=level: state.has(f"{lvl} - Stage Completion", world.player,
world.options.ow_boss_requirement.value))
set_rule(world.multiworld.get_entrance("To Level 6", world.player),
lambda state: state.has("Heart Star", world.player, world.required_heart_stars))

View File

@@ -483,6 +483,8 @@ def create_regions(multiworld: MultiWorld, player: int, options):
for name, data in regions.items():
multiworld.regions.append(create_region(multiworld, player, name, data))
def connect_entrances(multiworld: MultiWorld, player: int):
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
@@ -500,6 +502,7 @@ def create_regions(multiworld: MultiWorld, player: int, options):
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
region = Region(name, player, multiworld)
if data.locations:

View File

@@ -6,15 +6,15 @@ from worlds.AutoWorld import WebWorld, World
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
from .Options import KH1Options, kh1_option_groups
from .Regions import create_regions
from .Regions import connect_entrances, create_regions
from .Rules import set_rules
from .Presets import kh1_option_presets
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
def launch_client():
from .Client import launch
launch_subprocess(launch, name="KH1 Client")
launch_component(launch, name="KH1 Client")
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
@@ -242,6 +242,9 @@ class KH1World(World):
def create_regions(self):
create_regions(self.multiworld, self.player, self.options)
def connect_entrances(self):
connect_entrances(self.multiworld, self.player)
def generate_early(self):
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]

View File

@@ -5,8 +5,10 @@ ModuleUpdate.update()
import os
import asyncio
import json
import requests
from pymem import pymem
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \
SupportAbility_Table, ActionAbility_Table, all_weapon_slot
from .Names import ItemName
from .WorldLocations import *
@@ -82,6 +84,7 @@ class KH2Context(CommonContext):
}
self.kh2seedname = None
self.kh2slotdata = None
self.mem_json = None
self.itemamount = {}
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
@@ -178,7 +181,8 @@ class KH2Context(CommonContext):
self.base_accessory_slots = 1
self.base_armor_slots = 1
self.base_item_slots = 3
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E,
0x2770, 0x2772]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -340,12 +344,8 @@ class KH2Context(CommonContext):
self.locations_checked |= new_locations
if cmd in {"DataPackage"}:
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
if "Kingdom Hearts 2" in args["data"]["games"]:
self.data_package_kh2_cache(args)
if "KeybladeAbilities" in self.kh2slotdata.keys():
# sora ability to slot
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
@@ -359,24 +359,9 @@ class KH2Context(CommonContext):
self.all_weapon_location_id = set(all_weapon_location_id)
try:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
if self.kh2_game_version is None:
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A9830
self.Slot1 = 0x2A23518
self.Journal = 0x7434E0
self.Shop = 0x7435D0
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
self.kh2_game_version = None
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking. {self.kh2_game_version}")
self.kh2connected = True
if not self.kh2:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
self.get_addresses()
except Exception as e:
if self.kh2connected:
@@ -385,6 +370,13 @@ class KH2Context(CommonContext):
self.serverconneced = True
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
def data_package_kh2_cache(self, args):
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
async def checkWorldLocations(self):
try:
currentworldint = self.kh2_read_byte(self.Now)
@@ -425,7 +417,6 @@ class KH2Context(CommonContext):
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels]
}
# TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3
for i in range(6):
for location, data in formDict[i][1].items():
formlevel = self.kh2_read_byte(self.Save + data.addrObtained)
@@ -469,9 +460,11 @@ class KH2Context(CommonContext):
if locationName in self.chest_set:
if locationName in self.location_name_to_worlddata.keys():
locationData = self.location_name_to_worlddata[locationName]
if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
if self.kh2_read_byte(
self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
roomData = self.kh2_read_byte(self.Save + locationData.addrObtained)
self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex)
self.kh2_write_byte(self.Save + locationData.addrObtained,
roomData | 0x01 << locationData.bitIndex)
except Exception as e:
if self.kh2connected:
@@ -494,6 +487,9 @@ class KH2Context(CommonContext):
async def give_item(self, item, location):
try:
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
while not self.lookup_id_to_item:
await asyncio.sleep(0.5)
itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname]
@@ -637,7 +633,8 @@ class KH2Context(CommonContext):
item_data = self.item_name_to_data[item_name]
# if the inventory slot for that keyblade is less than the amount they should have,
# and they are not in stt
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13:
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(
self.Save + 0x1CFF) != 13:
# Checking form anchors for the keyblade to remove extra keyblades
if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \
or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \
@@ -738,7 +735,8 @@ class KH2Context(CommonContext):
item_data = self.item_name_to_data[item_name]
amount_of_items = 0
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name]
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}:
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(
self.Shop) in {10, 8}:
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
for item_name in master_stat:
@@ -797,7 +795,8 @@ class KH2Context(CommonContext):
# self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
if "PoptrackerVersionCheck" in self.kh2slotdata:
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(
self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
self.kh2_write_byte(self.Save + 0x3607, 1)
except Exception as e:
@@ -806,10 +805,59 @@ class KH2Context(CommonContext):
logger.info(e)
logger.info("line 840")
def get_addresses(self):
if not self.kh2connected and self.kh2 is not None:
if self.kh2_game_version is None:
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A9830
self.Slot1 = 0x2A23518
self.Journal = 0x7434E0
self.Shop = 0x7435D0
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
if self.game_communication_path:
logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment")
#if mem addresses file is found then check version and if old get new one
kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json")
if not os.path.exists(kh2memaddresses_path):
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
if mem_resp.status_code == 200:
self.mem_json = json.loads(mem_resp.content)
with open(kh2memaddresses_path,
'w') as f:
f.write(json.dumps(self.mem_json, indent=4))
else:
with open(kh2memaddresses_path, 'r') as f:
self.mem_json = json.load(f)
if self.mem_json:
for key in self.mem_json.keys():
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
self.Now = int(self.mem_json[key]["Now"], 0)
self.Save = int(self.mem_json[key]["Save"], 0)
self.Slot1 = int(self.mem_json[key]["Slot1"], 0)
self.Journal = int(self.mem_json[key]["Journal"], 0)
self.Shop = int(self.mem_json[key]["Shop"], 0)
self.kh2_game_version = key
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {self.kh2_game_version}")
self.kh2connected = True
else:
logger.info("Your game version does not match what the client requires. Check in the "
"kingdom-hearts-2-final-mix channel for more information on correcting the game "
"version.")
self.kh2connected = False
def finishedGame(ctx: KH2Context):
if ctx.kh2slotdata['FinalXemnas'] == 1:
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
if not ctx.final_xemnas and ctx.kh2_read_byte(
ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
ctx.final_xemnas = True
# three proofs
@@ -843,7 +891,8 @@ def finishedGame(ctx: KH2Context):
for boss in ctx.kh2slotdata["hitlist"]:
if boss in locations:
ctx.hitlist_bounties += 1
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"][
"Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
@@ -894,24 +943,7 @@ async def kh2_watcher(ctx: KH2Context):
while not ctx.kh2connected and ctx.serverconneced:
await asyncio.sleep(15)
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
if ctx.kh2 is not None:
if ctx.kh2_game_version is None:
if ctx.kh2_read_string(0x09A9830, 4) == "KH2J":
ctx.kh2_game_version = "STEAM"
ctx.Now = 0x0717008
ctx.Save = 0x09A9830
ctx.Slot1 = 0x2A23518
ctx.Journal = 0x7434E0
ctx.Shop = 0x7435D0
elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J":
ctx.kh2_game_version = "EGS"
else:
ctx.kh2_game_version = None
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
if ctx.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {ctx.kh2_game_version}")
ctx.kh2connected = True
ctx.get_addresses()
except Exception as e:
if ctx.kh2connected:
ctx.kh2connected = False

View File

@@ -540,7 +540,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
LocationName.SephirothFenrir,
LocationName.SephiEventLocation
],
RegionName.CoR: [
RegionName.CoR: [ #todo: make logic for getting these checks.
LocationName.CoRDepthsAPBoost,
LocationName.CoRDepthsPowerCrystal,
LocationName.CoRDepthsFrostCrystal,
@@ -1032,99 +1032,99 @@ def connect_regions(self):
multiworld = self.multiworld
player = self.player
# connecting every first visit to the GoA
KH2RegionConnections: typing.Dict[str, typing.Set[str]] = {
"Menu": {RegionName.GoA},
RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
KH2RegionConnections: typing.Dict[str, typing.Tuple[str]] = {
"Menu": (RegionName.GoA,),
RegionName.GoA: (RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
RegionName.LoD,
RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb,
RegionName.Dc, RegionName.Stt,
RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1,
RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master,
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne},
RegionName.LoD: {RegionName.ShanYu},
RegionName.ShanYu: {RegionName.LoD2},
RegionName.LoD2: {RegionName.AnsemRiku},
RegionName.AnsemRiku: {RegionName.StormRider},
RegionName.StormRider: {RegionName.DataXigbar},
RegionName.Ag: {RegionName.TwinLords},
RegionName.TwinLords: {RegionName.Ag2},
RegionName.Ag2: {RegionName.GenieJafar},
RegionName.GenieJafar: {RegionName.DataLexaeus},
RegionName.Dc: {RegionName.Tr},
RegionName.Tr: {RegionName.OldPete},
RegionName.OldPete: {RegionName.FuturePete},
RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia},
RegionName.Ha1: {RegionName.Ha2},
RegionName.Ha2: {RegionName.Ha3},
RegionName.Ha3: {RegionName.Ha4},
RegionName.Ha4: {RegionName.Ha5},
RegionName.Ha5: {RegionName.Ha6},
RegionName.Pr: {RegionName.Barbosa},
RegionName.Barbosa: {RegionName.Pr2},
RegionName.Pr2: {RegionName.GrimReaper1},
RegionName.GrimReaper1: {RegionName.GrimReaper2},
RegionName.GrimReaper2: {RegionName.DataLuxord},
RegionName.Oc: {RegionName.Cerberus},
RegionName.Cerberus: {RegionName.OlympusPete},
RegionName.OlympusPete: {RegionName.Hydra},
RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2},
RegionName.Oc2: {RegionName.Hades},
RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion},
RegionName.Oc2GofCup: {RegionName.HadesCups},
RegionName.Bc: {RegionName.Thresholder},
RegionName.Thresholder: {RegionName.Beast},
RegionName.Beast: {RegionName.DarkThorn},
RegionName.DarkThorn: {RegionName.Bc2},
RegionName.Bc2: {RegionName.Xaldin},
RegionName.Xaldin: {RegionName.DataXaldin},
RegionName.Sp: {RegionName.HostileProgram},
RegionName.HostileProgram: {RegionName.Sp2},
RegionName.Sp2: {RegionName.Mcp},
RegionName.Mcp: {RegionName.DataLarxene},
RegionName.Ht: {RegionName.PrisonKeeper},
RegionName.PrisonKeeper: {RegionName.OogieBoogie},
RegionName.OogieBoogie: {RegionName.Ht2},
RegionName.Ht2: {RegionName.Experiment},
RegionName.Experiment: {RegionName.DataVexen},
RegionName.Hb: {RegionName.Hb2},
RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx},
RegionName.HBDemyx: {RegionName.ThousandHeartless},
RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi},
RegionName.CoR: {RegionName.CorFirstFight},
RegionName.CorFirstFight: {RegionName.CorSecondFight},
RegionName.CorSecondFight: {RegionName.Transport},
RegionName.Pl: {RegionName.Scar},
RegionName.Scar: {RegionName.Pl2},
RegionName.Pl2: {RegionName.GroundShaker},
RegionName.GroundShaker: {RegionName.DataSaix},
RegionName.Stt: {RegionName.TwilightThorn},
RegionName.TwilightThorn: {RegionName.Axel1},
RegionName.Axel1: {RegionName.Axel2},
RegionName.Axel2: {RegionName.DataRoxas},
RegionName.Tt: {RegionName.Tt2},
RegionName.Tt2: {RegionName.Tt3},
RegionName.Tt3: {RegionName.DataAxel},
RegionName.Twtnw: {RegionName.Roxas},
RegionName.Roxas: {RegionName.Xigbar},
RegionName.Xigbar: {RegionName.Luxord},
RegionName.Luxord: {RegionName.Saix},
RegionName.Saix: {RegionName.Twtnw2},
RegionName.Twtnw2: {RegionName.Xemnas},
RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas},
RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2},
RegionName.ArmoredXemnas2: {RegionName.FinalXemnas},
RegionName.LevelsVS1: {RegionName.LevelsVS3},
RegionName.LevelsVS3: {RegionName.LevelsVS6},
RegionName.LevelsVS6: {RegionName.LevelsVS9},
RegionName.LevelsVS9: {RegionName.LevelsVS12},
RegionName.LevelsVS12: {RegionName.LevelsVS15},
RegionName.LevelsVS15: {RegionName.LevelsVS18},
RegionName.LevelsVS18: {RegionName.LevelsVS21},
RegionName.LevelsVS21: {RegionName.LevelsVS24},
RegionName.LevelsVS24: {RegionName.LevelsVS26},
RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo},
RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree},
RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour},
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne),
RegionName.LoD: (RegionName.ShanYu,),
RegionName.ShanYu: (RegionName.LoD2,),
RegionName.LoD2: (RegionName.AnsemRiku,),
RegionName.AnsemRiku: (RegionName.StormRider,),
RegionName.StormRider: (RegionName.DataXigbar,),
RegionName.Ag: (RegionName.TwinLords,),
RegionName.TwinLords: (RegionName.Ag2,),
RegionName.Ag2: (RegionName.GenieJafar,),
RegionName.GenieJafar: (RegionName.DataLexaeus,),
RegionName.Dc: (RegionName.Tr,),
RegionName.Tr: (RegionName.OldPete,),
RegionName.OldPete: (RegionName.FuturePete,),
RegionName.FuturePete: (RegionName.Terra, RegionName.DataMarluxia),
RegionName.Ha1: (RegionName.Ha2,),
RegionName.Ha2: (RegionName.Ha3,),
RegionName.Ha3: (RegionName.Ha4,),
RegionName.Ha4: (RegionName.Ha5,),
RegionName.Ha5: (RegionName.Ha6,),
RegionName.Pr: (RegionName.Barbosa,),
RegionName.Barbosa: (RegionName.Pr2,),
RegionName.Pr2: (RegionName.GrimReaper1,),
RegionName.GrimReaper1: (RegionName.GrimReaper2,),
RegionName.GrimReaper2: (RegionName.DataLuxord,),
RegionName.Oc: (RegionName.Cerberus,),
RegionName.Cerberus: (RegionName.OlympusPete,),
RegionName.OlympusPete: (RegionName.Hydra,),
RegionName.Hydra: (RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2),
RegionName.Oc2: (RegionName.Hades,),
RegionName.Hades: (RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion),
RegionName.Oc2GofCup: (RegionName.HadesCups,),
RegionName.Bc: (RegionName.Thresholder,),
RegionName.Thresholder: (RegionName.Beast,),
RegionName.Beast: (RegionName.DarkThorn,),
RegionName.DarkThorn: (RegionName.Bc2,),
RegionName.Bc2: (RegionName.Xaldin,),
RegionName.Xaldin: (RegionName.DataXaldin,),
RegionName.Sp: (RegionName.HostileProgram,),
RegionName.HostileProgram: (RegionName.Sp2,),
RegionName.Sp2: (RegionName.Mcp,),
RegionName.Mcp: (RegionName.DataLarxene,),
RegionName.Ht: (RegionName.PrisonKeeper,),
RegionName.PrisonKeeper: (RegionName.OogieBoogie,),
RegionName.OogieBoogie: (RegionName.Ht2,),
RegionName.Ht2: (RegionName.Experiment,),
RegionName.Experiment: (RegionName.DataVexen,),
RegionName.Hb: (RegionName.Hb2,),
RegionName.Hb2: (RegionName.CoR, RegionName.HBDemyx),
RegionName.HBDemyx: (RegionName.ThousandHeartless,),
RegionName.ThousandHeartless: (RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi),
RegionName.CoR: (RegionName.CorFirstFight,),
RegionName.CorFirstFight: (RegionName.CorSecondFight,),
RegionName.CorSecondFight: (RegionName.Transport,),
RegionName.Pl: (RegionName.Scar,),
RegionName.Scar: (RegionName.Pl2,),
RegionName.Pl2: (RegionName.GroundShaker,),
RegionName.GroundShaker: (RegionName.DataSaix,),
RegionName.Stt: (RegionName.TwilightThorn,),
RegionName.TwilightThorn: (RegionName.Axel1,),
RegionName.Axel1: (RegionName.Axel2,),
RegionName.Axel2: (RegionName.DataRoxas,),
RegionName.Tt: (RegionName.Tt2,),
RegionName.Tt2: (RegionName.Tt3,),
RegionName.Tt3: (RegionName.DataAxel,),
RegionName.Twtnw: (RegionName.Roxas,),
RegionName.Roxas: (RegionName.Xigbar,),
RegionName.Xigbar: (RegionName.Luxord,),
RegionName.Luxord: (RegionName.Saix,),
RegionName.Saix: (RegionName.Twtnw2,),
RegionName.Twtnw2: (RegionName.Xemnas,),
RegionName.Xemnas: (RegionName.ArmoredXemnas, RegionName.DataXemnas),
RegionName.ArmoredXemnas: (RegionName.ArmoredXemnas2,),
RegionName.ArmoredXemnas2: (RegionName.FinalXemnas,),
RegionName.LevelsVS1: (RegionName.LevelsVS3,),
RegionName.LevelsVS3: (RegionName.LevelsVS6,),
RegionName.LevelsVS6: (RegionName.LevelsVS9,),
RegionName.LevelsVS9: (RegionName.LevelsVS12,),
RegionName.LevelsVS12: (RegionName.LevelsVS15,),
RegionName.LevelsVS15: (RegionName.LevelsVS18,),
RegionName.LevelsVS18: (RegionName.LevelsVS21,),
RegionName.LevelsVS21: (RegionName.LevelsVS24,),
RegionName.LevelsVS24: (RegionName.LevelsVS26,),
RegionName.AtlanticaSongOne: (RegionName.AtlanticaSongTwo,),
RegionName.AtlanticaSongTwo: (RegionName.AtlanticaSongThree,),
RegionName.AtlanticaSongThree: (RegionName.AtlanticaSongFour,),
}
for source, target in KH2RegionConnections.items():

View File

@@ -194,8 +194,8 @@ class KH2WorldRules(KH2Rules):
RegionName.Oc: lambda state: self.oc_unlocked(state, 1),
RegionName.Oc2: lambda state: self.oc_unlocked(state, 2),
#twtnw1 is actually the roxas fight region thus roxas requires 1 way to the dawn
RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2),
# These will be swapped and First Visit lock for twtnw is in development.
# RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2),
RegionName.Ht: lambda state: self.ht_unlocked(state, 1),
@@ -263,7 +263,10 @@ class KH2WorldRules(KH2Rules):
weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player)
for location in weapon_region.locations:
add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player))
if location.name in exclusion_table["WeaponSlots"]: # shop items and starting items are not in this list
exclusion_item = exclusion_table["WeaponSlots"][location.name]
add_rule(location, lambda state, e_item=exclusion_item: state.has(e_item, self.player))
if location.name in Goofy_Checks:
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
elif location.name in Donald_Checks:
@@ -919,8 +922,8 @@ class KH2FightRules(KH2Rules):
# normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus
# hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus
sephiroth_rules = {
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state),
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([gap_closer], state) >= 1,
"hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
}
return sephiroth_rules[self.fight_logic]

View File

@@ -3,7 +3,7 @@ from typing import List
from BaseClasses import Tutorial, ItemClassification
from Fill import fast_fill
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
from worlds.AutoWorld import World, WebWorld
from .Items import *
from .Locations import *
@@ -17,7 +17,7 @@ from .Subclasses import KH2Item
def launch_client():
from .Client import launch
launch_subprocess(launch, name="KH2Client")
launch_component(launch, name="KH2Client")
components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT))

View File

@@ -10,7 +10,7 @@
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea
2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago
@@ -52,7 +52,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
<h2 style="text-transform:none";>What the Mod Manager Should Look Like.</h2>
![image](https://i.imgur.com/Si4oZ8w.png)
![image](https://i.imgur.com/N0WJ8Qn.png)
<h2 style="text-transform:none";>Using the KH2 Client</h2>

View File

@@ -253,7 +253,7 @@ def isConsumable(item) -> bool:
class RequirementsSettings:
def __init__(self, options):
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG)
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB)
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos

View File

@@ -9,7 +9,7 @@ import re
import bsdiff4
import settings
from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World
from .Common import *
@@ -315,8 +315,6 @@ class LinksAwakeningWorld(World):
# Set up filter rules
# The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons
all_dungeon_items_to_fill = list(self.prefill_own_dungeons)
# set containing the list of all possible dungeon locations for the player
all_dungeon_locs = set()
@@ -327,9 +325,6 @@ class LinksAwakeningWorld(World):
for item in self.prefill_original_dungeon[dungeon_index]:
allowed_locations_by_item[item] = locs
# put the items for this dungeon in the list to fill
all_dungeon_items_to_fill.extend(self.prefill_original_dungeon[dungeon_index])
# ...and gather the list of all dungeon locations
all_dungeon_locs |= locs
# ...also set the rules for the dungeon
@@ -369,16 +364,27 @@ class LinksAwakeningWorld(World):
if allowed_locations_by_item[item] is all_dungeon_locs:
i += 3
return i
all_dungeon_items_to_fill = self.get_pre_fill_items()
all_dungeon_items_to_fill.sort(key=priority)
# Set up state
all_state = self.multiworld.get_all_state(use_cache=False)
# Remove dungeon items we are about to put in from the state so that we don't double count
for item in all_dungeon_items_to_fill:
all_state.remove(item)
partial_all_state = CollectionState(self.multiworld)
# Collect every item from the item pool and every pre-fill item like MultiWorld.get_all_state, except not our own pre-fill items.
for item in self.multiworld.itempool:
partial_all_state.collect(item, prevent_sweep=True)
for player in self.multiworld.player_ids:
if player == self.player:
# Don't collect the items we're about to place.
continue
subworld = self.multiworld.worlds[player]
for item in subworld.get_pre_fill_items():
partial_all_state.collect(item, prevent_sweep=True)
# Sweep to pick up already placed items that are reachable with everything but the dungeon items.
partial_all_state.sweep_for_advancements()
# Finally, fill!
fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
name_cache = {}
# Tries to associate an icon from another game with an icon we have

View File

@@ -136,6 +136,12 @@ class MeritousWorld(World):
def set_rules(self):
set_rules(self.multiworld, self.player)
if self.goal == 0:
self.multiworld.completion_condition[self.player] = lambda state: state.has_any(
["Victory", "Full Victory"], self.player)
else:
self.multiworld.completion_condition[self.player] = lambda state: state.has(
"Full Victory", self.player)
def generate_basic(self):
self.multiworld.get_location("Place of Power", self.player).place_locked_item(
@@ -166,13 +172,6 @@ class MeritousWorld(World):
self.multiworld.get_location(boss, self.player).place_locked_item(
self.create_item("Evolution Trap"))
if self.goal == 0:
self.multiworld.completion_condition[self.player] = lambda state: state.has_any(
["Victory", "Full Victory"], self.player)
else:
self.multiworld.completion_condition[self.player] = lambda state: state.has(
"Full Victory", self.player)
def fill_slot_data(self) -> dict:
return {
"goal": self.goal,

View File

@@ -175,7 +175,7 @@ class WeaknessPlando(OptionDict):
display_name = "Plando Weaknesses"
schema = Schema({
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14))
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 15))
}
})
default = {}

View File

@@ -135,41 +135,47 @@ def set_rules(world: "MM2World") -> None:
world.weapon_damage[weapon][i] = 0
for p_boss in world.options.plando_weakness:
boss = bosses[p_boss]
for p_weapon in world.options.plando_weakness[p_boss]:
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \
and not any(w != p_weapon
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]
for w in world.weapon_damage):
weapon = weapons_to_id[p_weapon]
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[weapon] \
and not any(w != weapon
and world.weapon_damage[w][boss] >= minimum_weakness_requirement[w]
for w in world.weapon_damage):
# we need to replace this weakness
weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon])
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
= world.options.plando_weakness[p_boss][p_weapon]
weakness = world.random.choice([key for key in world.weapon_damage if key != weapon])
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
world.weapon_damage[weapon][boss] = 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]
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[i]
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]
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
for weapon in (1, 6):
if (world.weapon_damage[weapon][9] >= minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][9] >= minimum_weakness_requirement[i]
for i in range(9) if i not in (1, 6))):
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
world.weapon_damage[weapon][9] = 0
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
world.weapon_damage[weakness][9] = minimum_weakness_requirement[weakness]
if (world.weapon_damage[1][11] >= minimum_weakness_requirement[1] and
not any(world.weapon_damage[i][11] >= minimum_weakness_requirement[i]
for i in range(9) if i != 1)):
# Atomic Fire cannot be Boobeam Trap's only weakness
world.weapon_damage[1][11] = 0
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
world.weapon_damage[weakness][11] = minimum_weakness_requirement[weakness]
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]
# final special case
# There's a vanilla crash if Time Stopper kills Wily phase 1
@@ -218,9 +224,10 @@ def set_rules(world: "MM2World") -> None:
# 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
# it should be impossible to be out of energy, simply because even if every boss took 1 from
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
# be able to cover
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
# Quick Boomerang and no other, it would only be 28 off from defeating all 9,
# which Metal Blade should be able to cover
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon])
for weapon in weapon_weight
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]

View File

@@ -20,6 +20,8 @@ class PathOption(Choice):
class HiddenChests(Range):
"""
Number of hidden chest checks added to the applicable biomes.
Note: The number of hidden chests that spawn per run in each biome varies.
You are expected do multiple runs to get all of your checks.
"""
display_name = "Hidden Chests per Biome"
range_start = 0
@@ -30,6 +32,8 @@ class HiddenChests(Range):
class PedestalChecks(Range):
"""
Number of checks that will spawn on pedestals in the applicable biomes.
Note: The number of pedestals that spawn per run in each biome varies.
You are expected do multiple runs to get all of your checks.
"""
display_name = "Pedestal Checks per Biome"
range_start = 0

View File

@@ -1,3 +1,16 @@
# 2.4.0
### Features
- New option `free_fly_blacklist` limits which cities can show up as a free fly location.
- Spoiler log and hint text for maps where a species can be found now use human-friendly labels.
- Added many item and location groups based on item type, location type, and location geography.
### Fixes
- Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize
event tickets.
# 2.3.0
### Features

View File

@@ -22,7 +22,7 @@ from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map,
set_free_fly, set_legendary_cave_entrances)
from .opponents import randomize_opponent_parties
from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions,
RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement)
RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement, OPTION_GROUPS)
from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilities, randomize_learnsets,
randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
@@ -63,6 +63,7 @@ class PokemonEmeraldWebWorld(WebWorld):
)
tutorials = [setup_en, setup_es, setup_sv]
option_groups = OPTION_GROUPS
class PokemonEmeraldSettings(settings.Group):

View File

@@ -33,6 +33,26 @@ VISITED_EVENT_NAME_TO_ID = {
"EVENT_VISITED_SOUTHERN_ISLAND": 17,
}
BLACKLIST_OPTION_TO_VISITED_EVENT = {
"Littleroot Town": "EVENT_VISITED_LITTLEROOT_TOWN",
"Oldale Town": "EVENT_VISITED_OLDALE_TOWN",
"Petalburg City": "EVENT_VISITED_PETALBURG_CITY",
"Rustboro City": "EVENT_VISITED_RUSTBORO_CITY",
"Dewford Town": "EVENT_VISITED_DEWFORD_TOWN",
"Slateport City": "EVENT_VISITED_SLATEPORT_CITY",
"Mauville City": "EVENT_VISITED_MAUVILLE_CITY",
"Verdanturf Town": "EVENT_VISITED_VERDANTURF_TOWN",
"Fallarbor Town": "EVENT_VISITED_FALLARBOR_TOWN",
"Lavaridge Town": "EVENT_VISITED_LAVARIDGE_TOWN",
"Fortree City": "EVENT_VISITED_FORTREE_CITY",
"Lilycove City": "EVENT_VISITED_LILYCOVE_CITY",
"Mossdeep City": "EVENT_VISITED_MOSSDEEP_CITY",
"Sootopolis City": "EVENT_VISITED_SOOTOPOLIS_CITY",
"Ever Grande City": "EVENT_VISITED_EVER_GRANDE_CITY",
}
VISITED_EVENTS = frozenset(BLACKLIST_OPTION_TO_VISITED_EVENT.values())
class PokemonEmeraldLocation(Location):
game: str = "Pokemon Emerald"
@@ -129,18 +149,10 @@ def set_free_fly(world: "PokemonEmeraldWorld") -> None:
# If not enabled, set it to Littleroot Town by default
fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
if world.options.free_fly_location:
fly_location_name = world.random.choice([
"EVENT_VISITED_SLATEPORT_CITY",
"EVENT_VISITED_MAUVILLE_CITY",
"EVENT_VISITED_VERDANTURF_TOWN",
"EVENT_VISITED_FALLARBOR_TOWN",
"EVENT_VISITED_LAVARIDGE_TOWN",
"EVENT_VISITED_FORTREE_CITY",
"EVENT_VISITED_LILYCOVE_CITY",
"EVENT_VISITED_MOSSDEEP_CITY",
"EVENT_VISITED_SOOTOPOLIS_CITY",
"EVENT_VISITED_EVER_GRANDE_CITY",
])
blacklisted_locations = set(BLACKLIST_OPTION_TO_VISITED_EVENT[city] for city in world.options.free_fly_blacklist.value)
free_fly_locations = sorted(VISITED_EVENTS - blacklisted_locations)
if free_fly_locations:
fly_location_name = world.random.choice(free_fly_locations)
world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name]

View File

@@ -4,7 +4,7 @@ Option definitions for Pokemon Emerald
from dataclasses import dataclass
from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText,
PerGameCommonOptions)
PerGameCommonOptions, OptionGroup, StartInventory)
from .data import data
@@ -726,6 +726,39 @@ class FreeFlyLocation(Toggle):
display_name = "Free Fly Location"
class FreeFlyBlacklist(OptionSet):
"""
Disables specific locations as valid free fly locations.
Has no effect if Free Fly Location is disabled.
"""
display_name = "Free Fly Blacklist"
valid_keys = [
"Littleroot Town",
"Oldale Town",
"Petalburg City",
"Rustboro City",
"Dewford Town",
"Slateport City",
"Mauville City",
"Verdanturf Town",
"Fallarbor Town",
"Lavaridge Town",
"Fortree City",
"Lilycove City",
"Mossdeep City",
"Sootopolis City",
"Ever Grande City",
]
default = [
"Littleroot Town",
"Oldale Town",
"Petalburg City",
"Rustboro City",
"Dewford Town",
]
class HmRequirements(Choice):
"""
Sets the requirements to use HMs outside of battle.
@@ -785,6 +818,10 @@ class RandomizeFanfares(Toggle):
display_name = "Randomize Fanfares"
class PokemonEmeraldDeathLink(DeathLink):
__doc__ = DeathLink.__doc__ + "\n\n In Pokemon Emerald, whiting out sends a death and receiving a death causes you to white out."
class WonderTrading(DefaultOnToggle):
"""
Allows participation in wonder trading with other players in your current multiworld. Speak with the center receptionist on the second floor of any pokecenter.
@@ -810,6 +847,14 @@ class EasterEgg(FreeText):
default = "EMERALD SECRET"
class PokemonEmeraldStartInventory(StartInventory):
"""
Start with these items.
They will be in your PC, which you can access from your home or a pokemon center.
"""
@dataclass
class PokemonEmeraldOptions(PerGameCommonOptions):
goal: Goal
@@ -876,6 +921,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
extra_bumpy_slope: ExtraBumpySlope
modify_118: ModifyRoute118
free_fly_location: FreeFlyLocation
free_fly_blacklist: FreeFlyBlacklist
hm_requirements: HmRequirements
turbo_a: TurboA
@@ -885,7 +931,18 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
music: RandomizeMusic
fanfares: RandomizeFanfares
death_link: DeathLink
death_link: PokemonEmeraldDeathLink
enable_wonder_trading: WonderTrading
easter_egg: EasterEgg
start_inventory: PokemonEmeraldStartInventory
OPTION_GROUPS = [
OptionGroup(
"Item & Location Options", [
PokemonEmeraldStartInventory,
], True,
),
]

View File

@@ -5,7 +5,7 @@
- Sonic Adventure 2: Battle from: [Sonic Adventure 2: Battle Steam Store Page](https://store.steampowered.com/app/213610/Sonic_Adventure_2/)
- The Battle DLC is required if you choose to add Chao Karate locations to the randomizer
- SA Mod Manager from: [SA Mod Manager GitHub Releases Page](https://github.com/X-Hax/SA-Mod-Manager/releases)
- .NET Desktop Runtime 7.0 from: [.NET Desktop Runtime 7.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.9-windows-x64-installer)
- .NET Desktop Runtime 8.0 from: [.NET Desktop Runtime 8.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.12-windows-x64-installer)
- Archipelago Mod for Sonic Adventure 2: Battle
from: [Sonic Adventure 2: Battle Archipelago Randomizer Mod Releases Page](https://github.com/PoryGone/SA2B_Archipelago/releases/)
@@ -36,27 +36,23 @@
1. Install Sonic Adventure 2: Battle from Steam.
2. In the properties for Sonic Adventure 2 on Steam, force the use of Proton Experimental as the compatibility tool.
2. Launch the game at least once without mods.
3. Launch the game at least once without mods.
3. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle.
4. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle.
4. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
5. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool.
5. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is).
6. Run SAModManager.exe from Steam once. It should produce an error popup for a missing dependency, close the error.
6. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool.
7. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks).
7. Run SAModManager.exe from Steam once. It should produce an error popup saying you need .NET Desktop Runtime and ask you if you'd like to download it. Say yes and it will download through your browser.
8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer). If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0).
8. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks).
9. Right click the .NET 7 Desktop Runtime exe, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET 7 Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam.
9. Right click the .NET Desktop Runtime exe that was downloaded in step 6, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam.
6. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is).
8. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
10. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in SA Mod Manager.
@@ -77,7 +73,7 @@ Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rat
## Additional Options
Some additional settings related to the Archipelago messages in game can be adjusted in the SAModManager if you select `Configure Mod` on the SA2B_Archipelago mod. This settings will be under a `General Settings` tab.
- Message Display Count: This is the maximum number of Archipelago messages that can be displayed on screen at any given time.
- Message Display Duration: This dictates how long Archipelago messages are displayed on screen (in seconds).
- Message Font Size: The is the size of the font used to display the messages from Archipelago.
@@ -94,7 +90,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
- "The following mods didn't load correctly: SA2B_Archipelago: DLL error - The specified module could not be found."
- Make sure the `APCpp.dll` is in the same folder as the `sonic2app.exe`. (See Installation Procedures step 6)
- "sonic2app.exe - Entry Point Not Found"
- Make sure the `APCpp.dll` is up to date. Follow Installation Procedures step 6 to update the dll.
@@ -116,7 +112,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
1. Run the Launcher.exe which should be in the same folder as the your Sonic Adventure 2: Battle install.
2. Select the `Player` tab and reselect the controller for the player 1 input method.
3. Click the `Save settings and launch SONIC ADVENTURE 2` button. (Any mod manager settings will apply even if the game is launched this way rather than through the mod manager)
- Game crashes after display logos.
- This may be caused by a high monitor refresh rate.
- Change the monitor refresh rate to 60 Hz [Change display refresh rate on Windows] (https://support.microsoft.com/en-us/windows/change-your-display-refresh-rate-in-windows-c8ea729e-0678-015c-c415-f806f04aae5a)
@@ -125,13 +121,13 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
2. Select the `Compatibility` tab.
3. Check the `Run this program in compatility mode for:` box and select Windows 7 in the drop down.
4. Click the `Apply` button.
- No resolution options in the Launcher.exe.
- In the `Graphics device` dropdown, select the device and display you plan to run the game on. The `Resolution` dropdown should populate once a graphics device is selected.
- No music is playing in the game.
- If you enabled an `SADX Music` option, then most likely the music data was not copied properly into the mod folder (See Additional Options for instructions).
- Mission 1 is missing a texture in the stage select UI.
- Most likely another mod is conflicting and overwriting the texture pack. It is recommeded to have the SA2B Archipelago mod load last in the mod manager.

View File

@@ -245,7 +245,7 @@ class ShiversWorld(World):
storage_items += [self.create_item("Empty") for _ in range(3)]
state = self.multiworld.get_all_state(True)
state = self.multiworld.get_all_state(False)
self.random.shuffle(storage_locs)
self.random.shuffle(storage_items)

View File

@@ -9,6 +9,7 @@ configuration file.
All Ixupi pot pieces are randomized. Keys have been added to the game to lock off different rooms in the museum,
these are randomized. Crawling has been added and is required to use any crawl space.
Randomization can also control if Ixupi pots are in pieces, mixed, or complete, and in which worlds they will show up in.
## What is considered a location check in Shivers?
@@ -27,4 +28,5 @@ Victory is achieved when the player has captured the required number Ixupi set i
## Encountered a bug?
Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer.
Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer.
You may also open issues for the Shivers Randomizer Client [here](https://github.com/Shivers-Randomizer/Shivers-Randomizer/issues).

View File

@@ -5,12 +5,12 @@
- [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc
- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later
- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version
- [Shivers Randomizer Client](https://github.com/Shivers-Randomizer/Shivers-Randomizer/releases/latest) Latest release version
## Optional Software
- [PopTracker](https://github.com/black-sliver/PopTracker/releases/)
- [Jax's Shivers PopTracker pack](https://github.com/blazik-barth/Shivers-Tracker/releases/)
- [Shivers PopTracker pack](https://github.com/Shivers-Randomizer/Shivers-AP-Tracker/releases/latest)
## Setup ScummVM for Shivers
@@ -59,7 +59,9 @@ validator page: [YAML Validation page](/mysterycheck)
## What is a check
- Every puzzle
- Every puzzle hint/solution
- Every document that is considered a Flashback
- All puzzles
- All puzzle hints or solutions
- All documents that are considered Flashbacks
- All Ixupi captures (Lightning only if early)
- Optionally information plaques
- Optionally elevators

View File

@@ -48,6 +48,17 @@ class SM64World(World):
filler_count: int
star_costs: typing.Dict[str, int]
# Spoiler specific variable(s)
star_costs_spoiler_key_maxlen = len(max([
'First Floor Big Star Door',
'Basement Big Star Door',
'Second Floor Big Star Door',
'MIPS 1',
'MIPS 2',
'Endless Stairs',
], key=len))
def generate_early(self):
max_stars = 120
if (not self.options.enable_coin_stars):
@@ -238,3 +249,19 @@ class SM64World(World):
for location in region.locations:
er_hint_data[location.address] = entrance_name
hint_data[self.player] = er_hint_data
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
# Write calculated star costs to spoiler.
star_cost_spoiler_header = '\n\n' + self.player_name + ' Star Costs for Super Mario 64:\n\n'
spoiler_handle.write(star_cost_spoiler_header)
# - Reformat star costs dictionary in spoiler to be a bit more readable.
star_costs_spoiler = {}
star_costs_copy = self.star_costs.copy()
star_costs_spoiler['First Floor Big Star Door'] = star_costs_copy['FirstBowserDoorCost']
star_costs_spoiler['Basement Big Star Door'] = star_costs_copy['BasementDoorCost']
star_costs_spoiler['Second Floor Big Star Door'] = star_costs_copy['SecondFloorDoorCost']
star_costs_spoiler['MIPS 1'] = star_costs_copy['MIPS1Cost']
star_costs_spoiler['MIPS 2'] = star_costs_copy['MIPS2Cost']
star_costs_spoiler['Endless Stairs'] = star_costs_copy['StarsToFinish']
for star, cost in star_costs_spoiler.items():
spoiler_handle.write(f"{star:{self.star_costs_spoiler_key_maxlen}s} = {cost}\n")

View File

@@ -217,6 +217,10 @@ class SMZ3World(World):
SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys())
self.multiworld.state.smz3state[self.player] = TotalSMZ3Item.Progression([])
if not self.smz3World.Config.Keysanity:
# Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
def create_items(self):
self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World)
@@ -233,8 +237,6 @@ class SMZ3World(World):
progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems
else:
progressionItems = self.progression
# Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
for item in self.keyCardsItems:
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))

View File

@@ -1,6 +1,6 @@
import logging
from random import Random
from typing import Dict, Any, Iterable, Optional, List, TextIO
from typing import Dict, Any, Iterable, Optional, List, TextIO, cast
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from Options import PerGameCommonOptions
@@ -124,7 +124,7 @@ class StardewValleyWorld(World):
self.options)
def add_location(name: str, code: Optional[int], region: str):
region = world_regions[region]
region: Region = world_regions[region]
location = StardewLocation(self.player, name, code, region)
region.locations.append(location)
@@ -314,9 +314,9 @@ class StardewValleyWorld(World):
include_traps = True
exclude_island = False
for player in link_group["players"]:
player_options = self.multiworld.worlds[player].options
if self.multiworld.game[player] != self.game:
continue
player_options = cast(StardewValleyOptions, self.multiworld.worlds[player].options)
if player_options.trap_items == TrapItems.option_no_traps:
include_traps = False
if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true:

View File

@@ -4,7 +4,7 @@ from typing import List
from .bundle import Bundle, BundleTemplate
from ..content import StardewContent
from ..options import BundlePrice, StardewValleyOptions
from ..options import StardewValleyOptions
@dataclass

View File

@@ -10,15 +10,14 @@ from ...data.shop import ShopSource
from ...mods.mod_data import ModNames
from ...strings.craftable_names import ModEdible
from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit
from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem
from ...strings.fish_names import WaterItem, SVEWaterItem
from ...strings.flower_names import Flower
from ...strings.food_names import SVEMeal, SVEBeverage
from ...strings.forageable_names import Mushroom, Forageable, SVEForage
from ...strings.gift_names import SVEGift
from ...strings.metal_names import Ore
from ...strings.monster_drop_names import ModLoot, Loot
from ...strings.monster_drop_names import ModLoot
from ...strings.performance_names import Performance
from ...strings.region_names import Region, SVERegion, LogicRegion
from ...strings.region_names import Region, SVERegion
from ...strings.season_names import Season
from ...strings.seed_names import SVESeed
from ...strings.skill_names import Skill
@@ -81,7 +80,8 @@ register_mod_content_pack(SVEContentPack(
ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),),
ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),),
ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
SVEMeal.grampleton_orange_chicken: (
ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
@@ -92,7 +92,8 @@ register_mod_content_pack(SVEContentPack(
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
),
Mushroom.purple: (
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), )
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)),
ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), )
),
Mushroom.morel: (
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
@@ -117,7 +118,8 @@ register_mod_content_pack(SVEContentPack(
ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
other_requirements=(
CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),),
ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),),
SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
@@ -137,7 +139,8 @@ register_mod_content_pack(SVEContentPack(
SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
other_requirements=(
CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
# Fable Reef

View File

@@ -140,7 +140,7 @@ base_game = BaseGameContentPack(
Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),),
Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),),
Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),),
Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.winter,)),),
Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),),
Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),),

View File

@@ -6,7 +6,6 @@ from ...data.game_item import GenericSource, ItemTag
from ...data.harvest import HarvestCropSource
from ...strings.crop_names import Fruit
from ...strings.region_names import Region
from ...strings.season_names import Season
from ...strings.seed_names import Seed

View File

@@ -10,7 +10,7 @@ from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Ligh
from ..strings.crop_names import Fruit, Vegetable
from ..strings.currency_names import Currency
from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
from ..strings.fish_names import Fish, WaterItem, Trash, all_fish
from ..strings.fish_names import Fish, WaterItem, Trash
from ..strings.flower_names import Flower
from ..strings.food_names import Beverage, Meal
from ..strings.forageable_names import Forageable, Mushroom
@@ -832,7 +832,7 @@ calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg
magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50),
strawberry_seeds.as_amount(20),
spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5),
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),]
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5)]
calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2)
raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4)

View File

@@ -14,7 +14,7 @@ from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
from ..strings.fish_names import Fish, WaterItem, ModTrash, Trash
from ..strings.flower_names import Flower
from ..strings.food_names import Meal
from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
from ..strings.forageable_names import Forageable, DistantLandsForageable, Mushroom
from ..strings.gift_names import Gift
from ..strings.ingredient_names import Ingredient
from ..strings.machine_names import Machine
@@ -318,7 +318,8 @@ travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 25
preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1,
{MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30},
ModNames.archaeology)
restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology)
restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1},
ModNames.archaeology)
preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15,
ArtisanGood.oak_resin: 30}, ModNames.archaeology)
grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1},
@@ -330,12 +331,14 @@ glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifac
glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology)
bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology)
rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology)
rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology)
rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1},
ModNames.archaeology)
bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology)
water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology)
wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology)
hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology)
lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology)
lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1},
ModNames.archaeology)
volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1},
ModNames.archaeology)
haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1,

View File

@@ -2938,7 +2938,7 @@ id,region,name,tags,mod_name
7440,Farm,Craft Copper Slot Machine,"CRAFTSANITY",Luck Skill
7441,Farm,Craft Gold Slot Machine,"CRAFTSANITY",Luck Skill
7442,Farm,Craft Iridium Slot Machine,"CRAFTSANITY",Luck Skill
7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY",Luck Skill
7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY,GINGER_ISLAND",Luck Skill
7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic
7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded
1 id region name tags mod_name
2938 7440 Farm Craft Copper Slot Machine CRAFTSANITY Luck Skill
2939 7441 Farm Craft Gold Slot Machine CRAFTSANITY Luck Skill
2940 7442 Farm Craft Iridium Slot Machine CRAFTSANITY Luck Skill
2941 7443 Farm Craft Radioactive Slot Machine CRAFTSANITY CRAFTSANITY,GINGER_ISLAND Luck Skill
2942 7451 Adventurer's Guild Magic Elixir Recipe CHEFSANITY,CHEFSANITY_PURCHASE Magic
2943 7452 Adventurer's Guild Travel Core Recipe CRAFTSANITY Magic
2944 7453 Alesia Shop Haste Elixir Recipe CRAFTSANITY Stardew Valley Expanded

View File

@@ -1,15 +1,16 @@
from typing import Dict, List, Optional
from ..mods.mod_data import ModNames
from .recipe_source import RecipeSource, FriendshipSource, SkillSource, QueenOfSauceSource, ShopSource, StarterSource, ShopTradeSource, ShopFriendshipSource
from ..mods.mod_data import ModNames
from ..strings.animal_product_names import AnimalProduct
from ..strings.artisan_good_names import ArtisanGood
from ..strings.craftable_names import ModEdible, Edible
from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop
from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem
from ..strings.flower_names import Flower
from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
from ..strings.ingredient_names import Ingredient
from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal
from ..strings.forageable_names import Forageable, SVEForage, Mushroom
from ..strings.ingredient_names import Ingredient
from ..strings.material_names import Material
from ..strings.metal_names import Fossil, Artifact
from ..strings.monster_drop_names import Loot
@@ -45,7 +46,8 @@ def friendship_recipe(name: str, friend: str, hearts: int, ingredients: Dict[str
return create_recipe(name, ingredients, source, mod_name)
def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe:
def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int],
mod_name: Optional[str] = None) -> CookingRecipe:
source = ShopFriendshipSource(friend, hearts, region, price)
return create_recipe(name, ingredients, source, mod_name)
@@ -85,7 +87,8 @@ algae_soup = friendship_recipe(Meal.algae_soup, NPC.clint, 3, {WaterItem.green_a
artichoke_dip = queen_of_sauce_recipe(Meal.artichoke_dip, 1, Season.fall, 28, {Vegetable.artichoke: 1, AnimalProduct.cow_milk: 1})
autumn_bounty = friendship_recipe(Meal.autumn_bounty, NPC.demetrius, 7, {Vegetable.yam: 1, Vegetable.pumpkin: 1})
baked_fish = queen_of_sauce_recipe(Meal.baked_fish, 1, Season.summer, 7, {Fish.sunfish: 1, Fish.bream: 1, Ingredient.wheat_flour: 1})
banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30, {Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1})
banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30,
{Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1})
bean_hotpot = friendship_recipe(Meal.bean_hotpot, NPC.clint, 7, {Vegetable.green_bean: 2})
blackberry_cobbler_ingredients = {Forageable.blackberry: 2, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}
blackberry_cobbler_qos = queen_of_sauce_recipe(Meal.blackberry_cobbler, 2, Season.fall, 14, blackberry_cobbler_ingredients)
@@ -181,21 +184,23 @@ vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Ve
magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic)
baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15,
Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve)
Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve)
big_bark_burger = friendship_and_shop_recipe(SVEMeal.big_bark_burger, NPC.gus, 5, Region.saloon, 5500,
{SVEFish.puppyfish: 1, Meal.bread: 1, Ingredient.oil: 1}, ModNames.sve)
flower_cookie = shop_recipe(SVEMeal.flower_cookie, SVERegion.bear_shop, 0, {SVEForage.ferngill_primrose: 1, SVEForage.goldenrod: 1,
SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1,
AnimalProduct.large_egg: 1}, ModNames.sve)
SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1,
AnimalProduct.large_egg: 1}, ModNames.sve)
frog_legs = shop_recipe(SVEMeal.frog_legs, Region.adventurer_guild, 2000, {SVEFish.frog: 1, Ingredient.oil: 1, Ingredient.wheat_flour: 1}, ModNames.sve)
glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000,
{SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve)
mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6,
SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1},
ModNames.sve)
mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10,
Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve)
seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve)
mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500,
{SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2},
ModNames.sve)
seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1},
ModNames.sve)
void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000,
{SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve)
void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000,
@@ -205,17 +210,22 @@ mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.gobli
Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands)
void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands)
crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1,
DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands)
DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1},
ModNames.distant_lands)
pemmican = friendship_recipe(DistantLandsMeal.pemmican, ModNPC.goblin, 8, {Loot.bug_meat: 1, Fish.any: 1, Forageable.salmonberry: 3,
Material.stone: 2}, ModNames.distant_lands)
special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1,
Vegetable.garlic: 1}, ModNames.boarding_house)
diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology)
rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology)
ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology)
diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3,
{Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology)
rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1},
ModNames.archaeology)
ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9,
{WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1},
ModNames.archaeology)
grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill)
fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill)
all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes}
all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes}

View File

@@ -106,7 +106,7 @@ class MasterySource(RecipeSource):
self.skill = skill
def __repr__(self):
return f"MasterySource at level {self.level} {self.skill}"
return f"MasterySource {self.skill}"
class ShopSource(RecipeSource):

View File

@@ -1,7 +1,7 @@
import typing
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .cooking_logic import CookingLogicMixin
from .mine_logic import MineLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
@@ -13,6 +13,11 @@ from ..strings.region_names import Region
from ..strings.skill_names import Skill, ModSkill
from ..strings.tool_names import ToolMaterial, Tool
if typing.TYPE_CHECKING:
from ..mods.logic.mod_logic import ModLogicMixin
else:
ModLogicMixin = object
class AbilityLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
@@ -20,7 +25,8 @@ class AbilityLogicMixin(BaseLogicMixin):
self.ability = AbilityLogic(*args, **kwargs)
class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin]]):
class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin,
ModLogicMixin]]):
def can_mine_perfectly(self) -> StardewRule:
return self.logic.mine.can_progress_in_the_mines_from_floor(160)

View File

@@ -6,7 +6,6 @@ from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .tool_logic import ToolLogicMixin
from ..options import ToolProgression
from ..stardew_rule import StardewRule, True_
from ..strings.generic_names import Generic
from ..strings.geode_names import Geode

View File

@@ -1,3 +1,4 @@
import typing
from functools import cached_property
from typing import Union, Tuple
@@ -24,6 +25,11 @@ from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills
from ..strings.tool_names import ToolMaterial, Tool
from ..strings.wallet_item_names import Wallet
if typing.TYPE_CHECKING:
from ..mods.logic.mod_logic import ModLogicMixin
else:
ModLogicMixin = object
fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west)
vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level")
@@ -35,7 +41,7 @@ class SkillLogicMixin(BaseLogicMixin):
class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin,
CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin, ModLogicMixin]]):
# Should be cached
def can_earn_level(self, skill: str, level: int) -> StardewRule:

View File

@@ -2,7 +2,6 @@ from typing import Dict, Union
from ..mod_data import ModNames
from ... import options
from ...data.craftable_data import all_crafting_recipes_by_name
from ...logic.base_logic import BaseLogicMixin, BaseLogic
from ...logic.combat_logic import CombatLogicMixin
from ...logic.cooking_logic import CookingLogicMixin
@@ -20,11 +19,9 @@ from ...logic.season_logic import SeasonLogicMixin
from ...logic.skill_logic import SkillLogicMixin
from ...logic.time_logic import TimeLogicMixin
from ...logic.tool_logic import ToolLogicMixin
from ...options import Cropsanity
from ...stardew_rule import StardewRule, True_
from ...stardew_rule import StardewRule
from ...strings.artisan_good_names import ModArtisanGood
from ...strings.craftable_names import ModCraftable, ModMachine
from ...strings.fish_names import ModTrash
from ...strings.craftable_names import ModCraftable
from ...strings.ingredient_names import Ingredient
from ...strings.material_names import Material
from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil

View File

@@ -3,8 +3,8 @@ from typing import Dict, Union
from ..mod_data import ModNames
from ...logic.base_logic import BaseLogic, BaseLogicMixin
from ...logic.has_logic import HasLogicMixin
from ...logic.quest_logic import QuestLogicMixin
from ...logic.monster_logic import MonsterLogicMixin
from ...logic.quest_logic import QuestLogicMixin
from ...logic.received_logic import ReceivedLogicMixin
from ...logic.region_logic import RegionLogicMixin
from ...logic.relationship_logic import RelationshipLogicMixin
@@ -16,7 +16,6 @@ from ...strings.artisan_good_names import ArtisanGood
from ...strings.crop_names import Fruit, SVEFruit, SVEVegetable, Vegetable
from ...strings.fertilizer_names import Fertilizer
from ...strings.food_names import Meal, Beverage
from ...strings.forageable_names import SVEForage
from ...strings.material_names import Material
from ...strings.metal_names import Ore, MetalBar
from ...strings.monster_drop_names import Loot, ModLoot
@@ -35,7 +34,7 @@ class ModQuestLogicMixin(BaseLogicMixin):
class ModQuestLogic(BaseLogic[Union[HasLogicMixin, QuestLogicMixin, ReceivedLogicMixin, RegionLogicMixin,
TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
def get_modded_quest_rules(self) -> Dict[str, StardewRule]:
quests = dict()
quests.update(self._get_juna_quest_rules())

View File

@@ -66,7 +66,8 @@ class Goal(Choice):
class FarmType(Choice):
"""What farm to play on?"""
"""What farm to play on?
Custom farms are not supported"""
internal_name = "farm_type"
display_name = "Farm Type"
default = "random"
@@ -203,7 +204,7 @@ class SeasonRandomization(Choice):
class Cropsanity(Choice):
"""Formerly named "Seed Shuffle"
"""
Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs.
Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops
Enabled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop
@@ -233,9 +234,9 @@ class BackpackProgression(Choice):
class ToolProgression(Choice):
"""Shuffle the tool upgrades?
Vanilla: Clint will upgrade your tools with metal bars.
Progressive: You will randomly find Progressive Tool upgrades.
Cheap: Tool Upgrades will cost 2/5th as much
Very Cheap: Tool Upgrades will cost 1/5th as much"""
Progressive: Your tools upgrades are randomized.
Cheap: Tool Upgrades have a 60% discount
Very Cheap: Tool Upgrades have an 80% discount"""
internal_name = "tool_progression"
display_name = "Tool Progression"
default = 1
@@ -279,8 +280,8 @@ class BuildingProgression(Choice):
Vanilla: You can buy each building normally.
Progressive: You will receive the buildings and will be able to build the first one of each type for free,
once it is received. If you want more of the same building, it will cost the vanilla price.
Cheap: Buildings will cost half as much
Very Cheap: Buildings will cost 1/5th as much
Cheap: Buildings will have a 50% discount
Very Cheap: Buildings will an 80% discount
"""
internal_name = "building_progression"
display_name = "Building Progression"
@@ -327,7 +328,7 @@ class ArcadeMachineLocations(Choice):
class SpecialOrderLocations(Choice):
"""Shuffle Special Orders?
Disabled: The special orders are not included in the Archipelago shuffling.
Vanilla: The special orders are not included in the Archipelago shuffling. You may need to complete some of them anyway for their vanilla rewards
Board Only: The Special Orders on the board in town are location checks
Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town
Short: All Special Order requirements are reduced by 40%
@@ -377,12 +378,12 @@ class QuestLocations(NamedRange):
class Fishsanity(Choice):
"""Locations for catching a fish the first time?
"""Locations for catching each fish the first time?
None: There are no locations for catching fish
Legendaries: Each of the 5 legendary fish are checks, plus the extended family if qi board is turned on
Special: A curated selection of strong fish are checks
Randomized: A random selection of fish are checks
All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal
All: Every single fish in the game is a location that contains an item.
Exclude Legendaries: Every fish except legendaries
Exclude Hard Fish: Every fish under difficulty 80
Only Easy Fish: Every fish under difficulty 50
@@ -517,7 +518,7 @@ class Chefsanity(NamedRange):
class Craftsanity(Choice):
"""Checks for crafting items?
If enabled, all recipes purchased in shops will be checks as well.
Recipes obtained from other sources will depend on related archipelago settings
Recipes obtained from other sources will depend on their respective archipelago settings
"""
internal_name = "craftsanity"
display_name = "Craftsanity"
@@ -530,9 +531,9 @@ class Friendsanity(Choice):
"""Shuffle Friendships?
None: Friendship hearts are earned normally
Bachelors: Hearts with bachelors are shuffled
Starting NPCs: Hearts for NPCs available immediately are checks
All: Hearts for all npcs are checks, including Leo, Kent, Sandy, etc
All With Marriage: Hearts for all npcs are checks, including romance hearts up to 14 when applicable
Starting NPCs: Hearts for NPCs available immediately are shuffled
All: Hearts for all npcs are shuffled, including Leo, Kent, Sandy, etc
All With Marriage: All hearts for all npcs are shuffled, including romance hearts up to 14 when applicable
"""
internal_name = "friendsanity"
display_name = "Friendsanity"
@@ -577,7 +578,7 @@ class Walnutsanity(OptionSet):
"""Shuffle walnuts?
Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame
Bushes: Walnuts that are in a bush and can be collected by clicking it
Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
Dig Spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc)
"""
internal_name = "walnutsanity"
@@ -612,7 +613,7 @@ class NumberOfMovementBuffs(Range):
class EnabledFillerBuffs(OptionSet):
"""Enable various permanent player buffs to roll as filler items
Luck: Increase daily luck
Luck: Increased daily luck
Damage: Increased Damage %
Defense: Increased Defense
Immunity: Increased Immunity
@@ -637,7 +638,7 @@ class EnabledFillerBuffs(OptionSet):
class ExcludeGingerIsland(Toggle):
"""Exclude Ginger Island?
This option will forcefully exclude everything related to Ginger Island from the slot.
If you pick a goal that requires Ginger Island, you cannot exclude it and it will get included anyway"""
If you pick a goal that requires Ginger Island, this option will get forced to 'false'"""
internal_name = "exclude_ginger_island"
display_name = "Exclude Ginger Island"
default = 0

View File

@@ -122,7 +122,7 @@ medium_settings = {
options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs,
options.FriendsanityHeartSize.internal_name: 4,
options.Booksanity.internal_name: options.Booksanity.option_power_skill,
options.Walnutsanity.internal_name: [WalnutsanityOptionName.puzzles],
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none,
options.NumberOfMovementBuffs.internal_name: 6,
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,

View File

@@ -7,7 +7,7 @@ from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod
from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions
from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag
from .strings.entrance_names import Entrance, LogicEntrance
from .strings.region_names import Region, LogicRegion
from .strings.region_names import Region as RegionName, LogicRegion
class RegionFactory(Protocol):
@@ -16,192 +16,192 @@ class RegionFactory(Protocol):
vanilla_regions = [
RegionData(Region.menu, [Entrance.to_stardew_valley]),
RegionData(Region.stardew_valley, [Entrance.to_farmhouse]),
RegionData(Region.farm_house,
RegionData(RegionName.menu, [Entrance.to_stardew_valley]),
RegionData(RegionName.stardew_valley, [Entrance.to_farmhouse]),
RegionData(RegionName.farm_house,
[Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]),
RegionData(Region.cellar),
RegionData(Region.farm,
RegionData(RegionName.cellar),
RegionData(RegionName.farm,
[Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse,
Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops,
LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]),
RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]),
RegionData(Region.bus_stop,
RegionData(RegionName.backwoods, [Entrance.backwoods_to_mountain]),
RegionData(RegionName.bus_stop,
[Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]),
RegionData(Region.forest,
RegionData(RegionName.forest,
[Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch,
Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant,
LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby,
LogicEntrance.attend_festival_of_ice]),
RegionData(LogicRegion.forest_waterfall),
RegionData(Region.farm_cave),
RegionData(Region.greenhouse,
RegionData(RegionName.farm_cave),
RegionData(RegionName.greenhouse,
[LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse,
LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]),
RegionData(Region.mountain,
RegionData(RegionName.mountain,
[Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop,
Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild,
Entrance.mountain_to_town, Entrance.mountain_to_maru_room,
Entrance.mountain_to_leo_treehouse]),
RegionData(Region.leo_treehouse, is_ginger_island=True),
RegionData(Region.maru_room),
RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]),
RegionData(Region.bus_tunnel),
RegionData(Region.town,
RegionData(RegionName.leo_treehouse, is_ginger_island=True),
RegionData(RegionName.maru_room),
RegionData(RegionName.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]),
RegionData(RegionName.bus_tunnel),
RegionData(RegionName.town,
[Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store,
Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house,
Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart,
Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair,
LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]),
RegionData(Region.beach,
RegionData(RegionName.beach,
[Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau,
LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]),
RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]),
RegionData(Region.ranch),
RegionData(Region.leah_house),
RegionData(Region.mastery_cave),
RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]),
RegionData(Region.mutant_bug_lair),
RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
RegionData(Region.wizard_basement),
RegionData(Region.tent),
RegionData(Region.carpenter, [Entrance.enter_sebastian_room]),
RegionData(Region.sebastian_room),
RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]),
RegionData(Region.adventurer_guild_bedroom),
RegionData(Region.community_center,
RegionData(RegionName.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]),
RegionData(RegionName.ranch),
RegionData(RegionName.leah_house),
RegionData(RegionName.mastery_cave),
RegionData(RegionName.sewer, [Entrance.enter_mutant_bug_lair]),
RegionData(RegionName.mutant_bug_lair),
RegionData(RegionName.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
RegionData(RegionName.wizard_basement),
RegionData(RegionName.tent),
RegionData(RegionName.carpenter, [Entrance.enter_sebastian_room]),
RegionData(RegionName.sebastian_room),
RegionData(RegionName.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]),
RegionData(RegionName.adventurer_guild_bedroom),
RegionData(RegionName.community_center,
[Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank,
Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]),
RegionData(Region.crafts_room),
RegionData(Region.pantry),
RegionData(Region.fish_tank),
RegionData(Region.boiler_room),
RegionData(Region.bulletin_board),
RegionData(Region.vault),
RegionData(Region.hospital, [Entrance.enter_harvey_room]),
RegionData(Region.harvey_room),
RegionData(Region.pierre_store, [Entrance.enter_sunroom]),
RegionData(Region.sunroom),
RegionData(Region.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]),
RegionData(Region.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
RegionData(Region.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
RegionData(Region.jotpk_world_3),
RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
RegionData(Region.junimo_kart_4),
RegionData(Region.alex_house),
RegionData(Region.trailer),
RegionData(Region.mayor_house),
RegionData(Region.sam_house),
RegionData(Region.haley_house),
RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]),
RegionData(Region.museum),
RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]),
RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]),
RegionData(Region.movie_ticket_stand),
RegionData(Region.movie_theater),
RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]),
RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True),
RegionData(Region.elliott_house),
RegionData(Region.tide_pools),
RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]),
RegionData(Region.locker_room, [Entrance.enter_public_bath]),
RegionData(Region.public_bath),
RegionData(Region.witch_warp_cave, [Entrance.enter_witch_swamp]),
RegionData(Region.witch_swamp, [Entrance.enter_witch_hut]),
RegionData(Region.witch_hut, [Entrance.witch_warp_to_wizard_basement]),
RegionData(Region.quarry, [Entrance.enter_quarry_mine_entrance]),
RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]),
RegionData(Region.quarry_mine),
RegionData(Region.secret_woods),
RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]),
RegionData(Region.oasis, [Entrance.enter_casino]),
RegionData(Region.casino),
RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]),
RegionData(Region.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]),
RegionData(Region.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]),
RegionData(Region.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]),
RegionData(Region.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]),
RegionData(Region.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]),
RegionData(Region.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]),
RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]),
RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]),
RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]),
RegionData(Region.dangerous_skull_cavern, is_ginger_island=True),
RegionData(Region.island_south,
RegionData(RegionName.crafts_room),
RegionData(RegionName.pantry),
RegionData(RegionName.fish_tank),
RegionData(RegionName.boiler_room),
RegionData(RegionName.bulletin_board),
RegionData(RegionName.vault),
RegionData(RegionName.hospital, [Entrance.enter_harvey_room]),
RegionData(RegionName.harvey_room),
RegionData(RegionName.pierre_store, [Entrance.enter_sunroom]),
RegionData(RegionName.sunroom),
RegionData(RegionName.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]),
RegionData(RegionName.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
RegionData(RegionName.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
RegionData(RegionName.jotpk_world_3),
RegionData(RegionName.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
RegionData(RegionName.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
RegionData(RegionName.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
RegionData(RegionName.junimo_kart_4),
RegionData(RegionName.alex_house),
RegionData(RegionName.trailer),
RegionData(RegionName.mayor_house),
RegionData(RegionName.sam_house),
RegionData(RegionName.haley_house),
RegionData(RegionName.blacksmith, [LogicEntrance.blacksmith_copper]),
RegionData(RegionName.museum),
RegionData(RegionName.jojamart, [Entrance.enter_abandoned_jojamart]),
RegionData(RegionName.abandoned_jojamart, [Entrance.enter_movie_theater]),
RegionData(RegionName.movie_ticket_stand),
RegionData(RegionName.movie_theater),
RegionData(RegionName.fish_shop, [Entrance.fish_shop_to_boat_tunnel]),
RegionData(RegionName.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True),
RegionData(RegionName.elliott_house),
RegionData(RegionName.tide_pools),
RegionData(RegionName.bathhouse_entrance, [Entrance.enter_locker_room]),
RegionData(RegionName.locker_room, [Entrance.enter_public_bath]),
RegionData(RegionName.public_bath),
RegionData(RegionName.witch_warp_cave, [Entrance.enter_witch_swamp]),
RegionData(RegionName.witch_swamp, [Entrance.enter_witch_hut]),
RegionData(RegionName.witch_hut, [Entrance.witch_warp_to_wizard_basement]),
RegionData(RegionName.quarry, [Entrance.enter_quarry_mine_entrance]),
RegionData(RegionName.quarry_mine_entrance, [Entrance.enter_quarry_mine]),
RegionData(RegionName.quarry_mine),
RegionData(RegionName.secret_woods),
RegionData(RegionName.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]),
RegionData(RegionName.oasis, [Entrance.enter_casino]),
RegionData(RegionName.casino),
RegionData(RegionName.skull_cavern_entrance, [Entrance.enter_skull_cavern]),
RegionData(RegionName.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]),
RegionData(RegionName.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]),
RegionData(RegionName.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]),
RegionData(RegionName.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]),
RegionData(RegionName.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]),
RegionData(RegionName.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]),
RegionData(RegionName.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]),
RegionData(RegionName.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]),
RegionData(RegionName.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]),
RegionData(RegionName.dangerous_skull_cavern, is_ginger_island=True),
RegionData(RegionName.island_south,
[Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast,
Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site,
Entrance.parrot_express_docks_to_jungle],
is_ginger_island=True),
RegionData(Region.island_resort, is_ginger_island=True),
RegionData(Region.island_west,
RegionData(RegionName.island_resort, is_ginger_island=True),
RegionData(RegionName.island_west,
[Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave,
Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks,
Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island,
LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island,
LogicEntrance.grow_indoor_crops_on_island],
is_ginger_island=True),
RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True),
RegionData(Region.island_shrine, is_ginger_island=True),
RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True),
RegionData(Region.island_north,
RegionData(RegionName.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True),
RegionData(RegionName.island_shrine, is_ginger_island=True),
RegionData(RegionName.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True),
RegionData(RegionName.island_north,
[Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano,
Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks],
is_ginger_island=True),
RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True),
RegionData(Region.volcano_secret_beach, is_ginger_island=True),
RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True),
RegionData(Region.volcano_dwarf_shop, is_ginger_island=True),
RegionData(Region.volcano_floor_10, is_ginger_island=True),
RegionData(Region.island_trader, is_ginger_island=True),
RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True),
RegionData(Region.gourmand_frog_cave, is_ginger_island=True),
RegionData(Region.colored_crystals_cave, is_ginger_island=True),
RegionData(Region.shipwreck, is_ginger_island=True),
RegionData(Region.qi_walnut_room, is_ginger_island=True),
RegionData(Region.leo_hut, is_ginger_island=True),
RegionData(Region.pirate_cove, is_ginger_island=True),
RegionData(Region.field_office, is_ginger_island=True),
RegionData(Region.dig_site,
RegionData(RegionName.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True),
RegionData(RegionName.volcano_secret_beach, is_ginger_island=True),
RegionData(RegionName.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True),
RegionData(RegionName.volcano_dwarf_shop, is_ginger_island=True),
RegionData(RegionName.volcano_floor_10, is_ginger_island=True),
RegionData(RegionName.island_trader, is_ginger_island=True),
RegionData(RegionName.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True),
RegionData(RegionName.gourmand_frog_cave, is_ginger_island=True),
RegionData(RegionName.colored_crystals_cave, is_ginger_island=True),
RegionData(RegionName.shipwreck, is_ginger_island=True),
RegionData(RegionName.qi_walnut_room, is_ginger_island=True),
RegionData(RegionName.leo_hut, is_ginger_island=True),
RegionData(RegionName.pirate_cove, is_ginger_island=True),
RegionData(RegionName.field_office, is_ginger_island=True),
RegionData(RegionName.dig_site,
[Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano,
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle],
is_ginger_island=True),
RegionData(Region.professor_snail_cave, is_ginger_island=True),
RegionData(Region.coop),
RegionData(Region.barn),
RegionData(Region.shed),
RegionData(Region.slime_hutch),
RegionData(RegionName.professor_snail_cave, is_ginger_island=True),
RegionData(RegionName.coop),
RegionData(RegionName.barn),
RegionData(RegionName.shed),
RegionData(RegionName.slime_hutch),
RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf,
Entrance.dig_to_mines_floor_5]),
RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]),
RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]),
RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]),
RegionData(Region.mines_floor_20, [Entrance.dig_to_mines_floor_25]),
RegionData(Region.mines_floor_25, [Entrance.dig_to_mines_floor_30]),
RegionData(Region.mines_floor_30, [Entrance.dig_to_mines_floor_35]),
RegionData(Region.mines_floor_35, [Entrance.dig_to_mines_floor_40]),
RegionData(Region.mines_floor_40, [Entrance.dig_to_mines_floor_45]),
RegionData(Region.mines_floor_45, [Entrance.dig_to_mines_floor_50]),
RegionData(Region.mines_floor_50, [Entrance.dig_to_mines_floor_55]),
RegionData(Region.mines_floor_55, [Entrance.dig_to_mines_floor_60]),
RegionData(Region.mines_floor_60, [Entrance.dig_to_mines_floor_65]),
RegionData(Region.mines_floor_65, [Entrance.dig_to_mines_floor_70]),
RegionData(Region.mines_floor_70, [Entrance.dig_to_mines_floor_75]),
RegionData(Region.mines_floor_75, [Entrance.dig_to_mines_floor_80]),
RegionData(Region.mines_floor_80, [Entrance.dig_to_mines_floor_85]),
RegionData(Region.mines_floor_85, [Entrance.dig_to_mines_floor_90]),
RegionData(Region.mines_floor_90, [Entrance.dig_to_mines_floor_95]),
RegionData(Region.mines_floor_95, [Entrance.dig_to_mines_floor_100]),
RegionData(Region.mines_floor_100, [Entrance.dig_to_mines_floor_105]),
RegionData(Region.mines_floor_105, [Entrance.dig_to_mines_floor_110]),
RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]),
RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]),
RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]),
RegionData(Region.dangerous_mines_20, is_ginger_island=True),
RegionData(Region.dangerous_mines_60, is_ginger_island=True),
RegionData(Region.dangerous_mines_100, is_ginger_island=True),
RegionData(RegionName.mines, [LogicEntrance.talk_to_mines_dwarf,
Entrance.dig_to_mines_floor_5]),
RegionData(RegionName.mines_floor_5, [Entrance.dig_to_mines_floor_10]),
RegionData(RegionName.mines_floor_10, [Entrance.dig_to_mines_floor_15]),
RegionData(RegionName.mines_floor_15, [Entrance.dig_to_mines_floor_20]),
RegionData(RegionName.mines_floor_20, [Entrance.dig_to_mines_floor_25]),
RegionData(RegionName.mines_floor_25, [Entrance.dig_to_mines_floor_30]),
RegionData(RegionName.mines_floor_30, [Entrance.dig_to_mines_floor_35]),
RegionData(RegionName.mines_floor_35, [Entrance.dig_to_mines_floor_40]),
RegionData(RegionName.mines_floor_40, [Entrance.dig_to_mines_floor_45]),
RegionData(RegionName.mines_floor_45, [Entrance.dig_to_mines_floor_50]),
RegionData(RegionName.mines_floor_50, [Entrance.dig_to_mines_floor_55]),
RegionData(RegionName.mines_floor_55, [Entrance.dig_to_mines_floor_60]),
RegionData(RegionName.mines_floor_60, [Entrance.dig_to_mines_floor_65]),
RegionData(RegionName.mines_floor_65, [Entrance.dig_to_mines_floor_70]),
RegionData(RegionName.mines_floor_70, [Entrance.dig_to_mines_floor_75]),
RegionData(RegionName.mines_floor_75, [Entrance.dig_to_mines_floor_80]),
RegionData(RegionName.mines_floor_80, [Entrance.dig_to_mines_floor_85]),
RegionData(RegionName.mines_floor_85, [Entrance.dig_to_mines_floor_90]),
RegionData(RegionName.mines_floor_90, [Entrance.dig_to_mines_floor_95]),
RegionData(RegionName.mines_floor_95, [Entrance.dig_to_mines_floor_100]),
RegionData(RegionName.mines_floor_100, [Entrance.dig_to_mines_floor_105]),
RegionData(RegionName.mines_floor_105, [Entrance.dig_to_mines_floor_110]),
RegionData(RegionName.mines_floor_110, [Entrance.dig_to_mines_floor_115]),
RegionData(RegionName.mines_floor_115, [Entrance.dig_to_mines_floor_120]),
RegionData(RegionName.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]),
RegionData(RegionName.dangerous_mines_20, is_ginger_island=True),
RegionData(RegionName.dangerous_mines_60, is_ginger_island=True),
RegionData(RegionName.dangerous_mines_100, is_ginger_island=True),
RegionData(LogicRegion.mines_dwarf_shop),
RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]),
@@ -256,206 +256,207 @@ vanilla_regions = [
# Exists and where they lead
vanilla_connections = [
ConnectionData(Entrance.to_stardew_valley, Region.stardew_valley),
ConnectionData(Entrance.to_farmhouse, Region.farm_house),
ConnectionData(Entrance.farmhouse_to_farm, Region.farm),
ConnectionData(Entrance.downstairs_to_cellar, Region.cellar),
ConnectionData(Entrance.farm_to_backwoods, Region.backwoods),
ConnectionData(Entrance.farm_to_bus_stop, Region.bus_stop),
ConnectionData(Entrance.farm_to_forest, Region.forest),
ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.enter_greenhouse, Region.greenhouse),
ConnectionData(Entrance.enter_coop, Region.coop),
ConnectionData(Entrance.enter_barn, Region.barn),
ConnectionData(Entrance.enter_shed, Region.shed),
ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch),
ConnectionData(Entrance.use_desert_obelisk, Region.desert),
ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.use_farm_obelisk, Region.farm),
ConnectionData(Entrance.backwoods_to_mountain, Region.mountain),
ConnectionData(Entrance.bus_stop_to_town, Region.town),
ConnectionData(Entrance.bus_stop_to_tunnel_entrance, Region.tunnel_entrance),
ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, Region.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.take_bus_to_desert, Region.desert),
ConnectionData(Entrance.forest_to_town, Region.town),
ConnectionData(Entrance.forest_to_wizard_tower, Region.wizard_tower,
ConnectionData(Entrance.to_stardew_valley, RegionName.stardew_valley),
ConnectionData(Entrance.to_farmhouse, RegionName.farm_house),
ConnectionData(Entrance.farmhouse_to_farm, RegionName.farm),
ConnectionData(Entrance.downstairs_to_cellar, RegionName.cellar),
ConnectionData(Entrance.farm_to_backwoods, RegionName.backwoods),
ConnectionData(Entrance.farm_to_bus_stop, RegionName.bus_stop),
ConnectionData(Entrance.farm_to_forest, RegionName.forest),
ConnectionData(Entrance.farm_to_farmcave, RegionName.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.enter_greenhouse, RegionName.greenhouse),
ConnectionData(Entrance.enter_coop, RegionName.coop),
ConnectionData(Entrance.enter_barn, RegionName.barn),
ConnectionData(Entrance.enter_shed, RegionName.shed),
ConnectionData(Entrance.enter_slime_hutch, RegionName.slime_hutch),
ConnectionData(Entrance.use_desert_obelisk, RegionName.desert),
ConnectionData(Entrance.use_island_obelisk, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.use_farm_obelisk, RegionName.farm),
ConnectionData(Entrance.backwoods_to_mountain, RegionName.mountain),
ConnectionData(Entrance.bus_stop_to_town, RegionName.town),
ConnectionData(Entrance.bus_stop_to_tunnel_entrance, RegionName.tunnel_entrance),
ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, RegionName.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.take_bus_to_desert, RegionName.desert),
ConnectionData(Entrance.forest_to_town, RegionName.town),
ConnectionData(Entrance.forest_to_wizard_tower, RegionName.wizard_tower,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_wizard_basement, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_marnie_ranch, Region.ranch,
ConnectionData(Entrance.enter_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_marnie_ranch, RegionName.ranch,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.forest_to_leah_cottage, Region.leah_house,
ConnectionData(Entrance.forest_to_leah_cottage, RegionName.leah_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_secret_woods, Region.secret_woods),
ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES),
ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_railroad, Region.railroad),
ConnectionData(Entrance.mountain_to_tent, Region.tent,
ConnectionData(Entrance.enter_secret_woods, RegionName.secret_woods),
ConnectionData(Entrance.forest_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_mastery_cave, RegionName.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES),
ConnectionData(Entrance.town_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_mutant_bug_lair, RegionName.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_railroad, RegionName.railroad),
ConnectionData(Entrance.mountain_to_tent, RegionName.tent,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_leo_treehouse, Region.leo_treehouse,
ConnectionData(Entrance.mountain_to_leo_treehouse, RegionName.leo_treehouse,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.mountain_to_carpenter_shop, Region.carpenter,
ConnectionData(Entrance.mountain_to_carpenter_shop, RegionName.carpenter,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_maru_room, Region.maru_room,
ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
ConnectionData(Entrance.enter_sebastian_room, RegionName.sebastian_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_adventurer_guild, RegionName.adventurer_guild,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom),
ConnectionData(Entrance.enter_quarry, Region.quarry),
ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance,
ConnectionData(Entrance.adventurer_guild_to_bedroom, RegionName.adventurer_guild_bedroom),
ConnectionData(Entrance.enter_quarry, RegionName.quarry),
ConnectionData(Entrance.enter_quarry_mine_entrance, RegionName.quarry_mine_entrance,
flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_quarry_mine, Region.quarry_mine),
ConnectionData(Entrance.mountain_to_town, Region.town),
ConnectionData(Entrance.town_to_community_center, Region.community_center,
ConnectionData(Entrance.enter_quarry_mine, RegionName.quarry_mine),
ConnectionData(Entrance.mountain_to_town, RegionName.town),
ConnectionData(Entrance.town_to_community_center, RegionName.community_center,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.access_crafts_room, Region.crafts_room),
ConnectionData(Entrance.access_pantry, Region.pantry),
ConnectionData(Entrance.access_fish_tank, Region.fish_tank),
ConnectionData(Entrance.access_boiler_room, Region.boiler_room),
ConnectionData(Entrance.access_bulletin_board, Region.bulletin_board),
ConnectionData(Entrance.access_vault, Region.vault),
ConnectionData(Entrance.town_to_hospital, Region.hospital,
ConnectionData(Entrance.access_crafts_room, RegionName.crafts_room),
ConnectionData(Entrance.access_pantry, RegionName.pantry),
ConnectionData(Entrance.access_fish_tank, RegionName.fish_tank),
ConnectionData(Entrance.access_boiler_room, RegionName.boiler_room),
ConnectionData(Entrance.access_bulletin_board, RegionName.bulletin_board),
ConnectionData(Entrance.access_vault, RegionName.vault),
ConnectionData(Entrance.town_to_hospital, RegionName.hospital,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_harvey_room, Region.harvey_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_pierre_general_store, Region.pierre_store,
ConnectionData(Entrance.enter_harvey_room, RegionName.harvey_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_pierre_general_store, RegionName.pierre_store,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_sunroom, Region.sunroom, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_clint_blacksmith, Region.blacksmith,
ConnectionData(Entrance.enter_sunroom, RegionName.sunroom, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_clint_blacksmith, RegionName.blacksmith,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_saloon, Region.saloon,
ConnectionData(Entrance.town_to_saloon, RegionName.saloon,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.play_journey_of_the_prairie_king, Region.jotpk_world_1),
ConnectionData(Entrance.reach_jotpk_world_2, Region.jotpk_world_2),
ConnectionData(Entrance.reach_jotpk_world_3, Region.jotpk_world_3),
ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1),
ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2),
ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3),
ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4),
ConnectionData(Entrance.town_to_sam_house, Region.sam_house,
ConnectionData(Entrance.play_journey_of_the_prairie_king, RegionName.jotpk_world_1),
ConnectionData(Entrance.reach_jotpk_world_2, RegionName.jotpk_world_2),
ConnectionData(Entrance.reach_jotpk_world_3, RegionName.jotpk_world_3),
ConnectionData(Entrance.play_junimo_kart, RegionName.junimo_kart_1),
ConnectionData(Entrance.reach_junimo_kart_2, RegionName.junimo_kart_2),
ConnectionData(Entrance.reach_junimo_kart_3, RegionName.junimo_kart_3),
ConnectionData(Entrance.reach_junimo_kart_4, RegionName.junimo_kart_4),
ConnectionData(Entrance.town_to_sam_house, RegionName.sam_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_haley_house, Region.haley_house,
ConnectionData(Entrance.town_to_haley_house, RegionName.haley_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_mayor_manor, Region.mayor_house,
ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_alex_house, Region.alex_house,
ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_trailer, Region.trailer,
ConnectionData(Entrance.town_to_trailer, RegionName.trailer,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_museum, Region.museum,
ConnectionData(Entrance.town_to_museum, RegionName.museum,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_jojamart, Region.jojamart,
ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.purchase_movie_ticket, Region.movie_ticket_stand),
ConnectionData(Entrance.enter_abandoned_jojamart, Region.abandoned_jojamart),
ConnectionData(Entrance.enter_movie_theater, Region.movie_theater),
ConnectionData(Entrance.town_to_beach, Region.beach),
ConnectionData(Entrance.enter_elliott_house, Region.elliott_house,
ConnectionData(Entrance.purchase_movie_ticket, RegionName.movie_ticket_stand),
ConnectionData(Entrance.enter_abandoned_jojamart, RegionName.abandoned_jojamart),
ConnectionData(Entrance.enter_movie_theater, RegionName.movie_theater),
ConnectionData(Entrance.town_to_beach, RegionName.beach),
ConnectionData(Entrance.enter_elliott_house, RegionName.elliott_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.beach_to_willy_fish_shop, Region.fish_shop,
ConnectionData(Entrance.beach_to_willy_fish_shop, RegionName.fish_shop,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.fish_shop_to_boat_tunnel, Region.boat_tunnel,
ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_tide_pools, Region.tide_pools),
ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
ConnectionData(Entrance.boat_to_ginger_island, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_tide_pools, RegionName.tide_pools),
ConnectionData(Entrance.mountain_to_the_mines, RegionName.mines,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5),
ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10),
ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15),
ConnectionData(Entrance.dig_to_mines_floor_20, Region.mines_floor_20),
ConnectionData(Entrance.dig_to_mines_floor_25, Region.mines_floor_25),
ConnectionData(Entrance.dig_to_mines_floor_30, Region.mines_floor_30),
ConnectionData(Entrance.dig_to_mines_floor_35, Region.mines_floor_35),
ConnectionData(Entrance.dig_to_mines_floor_40, Region.mines_floor_40),
ConnectionData(Entrance.dig_to_mines_floor_45, Region.mines_floor_45),
ConnectionData(Entrance.dig_to_mines_floor_50, Region.mines_floor_50),
ConnectionData(Entrance.dig_to_mines_floor_55, Region.mines_floor_55),
ConnectionData(Entrance.dig_to_mines_floor_60, Region.mines_floor_60),
ConnectionData(Entrance.dig_to_mines_floor_65, Region.mines_floor_65),
ConnectionData(Entrance.dig_to_mines_floor_70, Region.mines_floor_70),
ConnectionData(Entrance.dig_to_mines_floor_75, Region.mines_floor_75),
ConnectionData(Entrance.dig_to_mines_floor_80, Region.mines_floor_80),
ConnectionData(Entrance.dig_to_mines_floor_85, Region.mines_floor_85),
ConnectionData(Entrance.dig_to_mines_floor_90, Region.mines_floor_90),
ConnectionData(Entrance.dig_to_mines_floor_95, Region.mines_floor_95),
ConnectionData(Entrance.dig_to_mines_floor_100, Region.mines_floor_100),
ConnectionData(Entrance.dig_to_mines_floor_105, Region.mines_floor_105),
ConnectionData(Entrance.dig_to_mines_floor_110, Region.mines_floor_110),
ConnectionData(Entrance.dig_to_mines_floor_115, Region.mines_floor_115),
ConnectionData(Entrance.dig_to_mines_floor_120, Region.mines_floor_120),
ConnectionData(Entrance.dig_to_dangerous_mines_20, Region.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_to_dangerous_mines_60, Region.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_to_dangerous_mines_100, Region.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_skull_cavern_entrance, Region.skull_cavern_entrance,
ConnectionData(Entrance.dig_to_mines_floor_5, RegionName.mines_floor_5),
ConnectionData(Entrance.dig_to_mines_floor_10, RegionName.mines_floor_10),
ConnectionData(Entrance.dig_to_mines_floor_15, RegionName.mines_floor_15),
ConnectionData(Entrance.dig_to_mines_floor_20, RegionName.mines_floor_20),
ConnectionData(Entrance.dig_to_mines_floor_25, RegionName.mines_floor_25),
ConnectionData(Entrance.dig_to_mines_floor_30, RegionName.mines_floor_30),
ConnectionData(Entrance.dig_to_mines_floor_35, RegionName.mines_floor_35),
ConnectionData(Entrance.dig_to_mines_floor_40, RegionName.mines_floor_40),
ConnectionData(Entrance.dig_to_mines_floor_45, RegionName.mines_floor_45),
ConnectionData(Entrance.dig_to_mines_floor_50, RegionName.mines_floor_50),
ConnectionData(Entrance.dig_to_mines_floor_55, RegionName.mines_floor_55),
ConnectionData(Entrance.dig_to_mines_floor_60, RegionName.mines_floor_60),
ConnectionData(Entrance.dig_to_mines_floor_65, RegionName.mines_floor_65),
ConnectionData(Entrance.dig_to_mines_floor_70, RegionName.mines_floor_70),
ConnectionData(Entrance.dig_to_mines_floor_75, RegionName.mines_floor_75),
ConnectionData(Entrance.dig_to_mines_floor_80, RegionName.mines_floor_80),
ConnectionData(Entrance.dig_to_mines_floor_85, RegionName.mines_floor_85),
ConnectionData(Entrance.dig_to_mines_floor_90, RegionName.mines_floor_90),
ConnectionData(Entrance.dig_to_mines_floor_95, RegionName.mines_floor_95),
ConnectionData(Entrance.dig_to_mines_floor_100, RegionName.mines_floor_100),
ConnectionData(Entrance.dig_to_mines_floor_105, RegionName.mines_floor_105),
ConnectionData(Entrance.dig_to_mines_floor_110, RegionName.mines_floor_110),
ConnectionData(Entrance.dig_to_mines_floor_115, RegionName.mines_floor_115),
ConnectionData(Entrance.dig_to_mines_floor_120, RegionName.mines_floor_120),
ConnectionData(Entrance.dig_to_dangerous_mines_20, RegionName.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_to_dangerous_mines_60, RegionName.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_to_dangerous_mines_100, RegionName.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_skull_cavern_entrance, RegionName.skull_cavern_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_oasis, Region.oasis,
ConnectionData(Entrance.enter_oasis, RegionName.oasis,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_casino, Region.casino, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_skull_cavern, Region.skull_cavern),
ConnectionData(Entrance.mine_to_skull_cavern_floor_25, Region.skull_cavern_25),
ConnectionData(Entrance.mine_to_skull_cavern_floor_50, Region.skull_cavern_50),
ConnectionData(Entrance.mine_to_skull_cavern_floor_75, Region.skull_cavern_75),
ConnectionData(Entrance.mine_to_skull_cavern_floor_100, Region.skull_cavern_100),
ConnectionData(Entrance.mine_to_skull_cavern_floor_125, Region.skull_cavern_125),
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, Region.skull_cavern_150),
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, Region.skull_cavern_175),
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, Region.skull_cavern_200),
ConnectionData(Entrance.enter_dangerous_skull_cavern, Region.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_witch_warp_cave, Region.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_swamp, Region.witch_swamp, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_hut, Region.witch_hut, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.witch_warp_to_wizard_basement, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_bathhouse_entrance, Region.bathhouse_entrance,
ConnectionData(Entrance.enter_casino, RegionName.casino, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_skull_cavern, RegionName.skull_cavern),
ConnectionData(Entrance.mine_to_skull_cavern_floor_25, RegionName.skull_cavern_25),
ConnectionData(Entrance.mine_to_skull_cavern_floor_50, RegionName.skull_cavern_50),
ConnectionData(Entrance.mine_to_skull_cavern_floor_75, RegionName.skull_cavern_75),
ConnectionData(Entrance.mine_to_skull_cavern_floor_100, RegionName.skull_cavern_100),
ConnectionData(Entrance.mine_to_skull_cavern_floor_125, RegionName.skull_cavern_125),
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, RegionName.skull_cavern_150),
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, RegionName.skull_cavern_175),
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, RegionName.skull_cavern_200),
ConnectionData(Entrance.enter_dangerous_skull_cavern, RegionName.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_witch_warp_cave, RegionName.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_swamp, RegionName.witch_swamp, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_hut, RegionName.witch_hut, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.witch_warp_to_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_bathhouse_entrance, RegionName.bathhouse_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_locker_room, Region.locker_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_public_bath, Region.public_bath, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_south_to_west, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_north, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_east, Region.island_east, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_southeast, Region.island_south_east,
ConnectionData(Entrance.enter_locker_room, RegionName.locker_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_public_bath, RegionName.public_bath, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_south_to_west, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_north, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_east, RegionName.island_east, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_southeast, RegionName.island_south_east,
flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse,
ConnectionData(Entrance.use_island_resort, RegionName.island_resort, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_islandfarmhouse, RegionName.island_farmhouse,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave,
ConnectionData(Entrance.island_west_to_gourmand_cave, RegionName.gourmand_frog_cave,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_crystals_cave, Region.colored_crystals_cave,
ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_shipwreck, Region.shipwreck,
ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_qi_walnut_room, Region.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_leo_hut, Region.leo_hut,
ConnectionData(Entrance.island_west_to_qi_walnut_room, RegionName.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_leo_hut, RegionName.leo_hut,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_island_shrine, Region.island_shrine,
ConnectionData(Entrance.island_east_to_island_shrine, RegionName.island_shrine,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_southeast_to_pirate_cove, Region.pirate_cove,
ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_north_to_field_office, Region.field_office,
ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_north_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_site_to_professor_snail_cave, Region.professor_snail_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_north_to_volcano, Region.volcano,
ConnectionData(Entrance.island_north_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_site_to_professor_snail_cave, RegionName.professor_snail_cave,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.volcano_to_secret_beach, Region.volcano_secret_beach,
ConnectionData(Entrance.island_north_to_volcano, RegionName.volcano,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_island_trader, Region.island_trader, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.climb_to_volcano_5, Region.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_volcano_dwarf, Region.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.climb_to_volcano_10, Region.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.volcano_to_secret_beach, RegionName.volcano_secret_beach,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_island_trader, RegionName.island_trader, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.climb_to_volcano_5, RegionName.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_volcano_dwarf, RegionName.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.climb_to_volcano_10, RegionName.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop),
@@ -708,7 +709,7 @@ def swap_connections_until_valid(regions_by_name, connections_by_name: Dict[str,
def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[ConnectionData]) -> bool:
if region_name == Region.menu:
if region_name == RegionName.menu:
return True
for connection in connections_in_slot:
if region_name == connection.destination:
@@ -718,11 +719,11 @@ def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[C
def find_reachable_regions(regions_by_name, connections_by_name,
randomized_connections: Dict[ConnectionData, ConnectionData]):
reachable_regions = {Region.menu}
reachable_regions = {RegionName.menu}
unreachable_regions = {region for region in regions_by_name.keys()}
# unreachable_regions = {region for region in regions_by_name.keys() if region_should_be_reachable(region, connections_by_name.values())}
unreachable_regions.remove(Region.menu)
exits_to_explore = list(regions_by_name[Region.menu].exits)
unreachable_regions.remove(RegionName.menu)
exits_to_explore = list(regions_by_name[RegionName.menu].exits)
while exits_to_explore:
exit_name = exits_to_explore.pop()
# if exit_name not in connections_by_name:

View File

@@ -12,7 +12,7 @@ from typing import List
from worlds.stardew_valley import LocationData
from worlds.stardew_valley.items import load_item_csv, Group, ItemData
from worlds.stardew_valley.locations import load_location_csv, LocationTags
from worlds.stardew_valley.locations import load_location_csv
RESOURCE_PACK_CODE_OFFSET = 5000
script_folder = Path(__file__)
@@ -56,9 +56,9 @@ if __name__ == "__main__":
and item.code_without_offset is not None) + 1)
resource_pack_counter = itertools.count(max(item.code_without_offset
for item in loaded_items
if Group.RESOURCE_PACK in item.groups
and item.code_without_offset is not None) + 1)
for item in loaded_items
if Group.RESOURCE_PACK in item.groups
and item.code_without_offset is not None) + 1)
items_to_write = []
for item in loaded_items:
if item.code_without_offset is None:

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
from functools import cached_property
from itertools import chain
from threading import Lock
from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional
from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional, cast
from BaseClasses import CollectionState
from .literal import true_, false_, LiteralStardewRule
@@ -318,6 +318,7 @@ class Or(AggregatingStardewRule):
return Or(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state)
if type(other) is Or:
other = cast(Or, other)
return Or(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules),
_simplification_state=self.simplification_state.merge(other.simplification_state))
@@ -344,6 +345,7 @@ class And(AggregatingStardewRule):
return And(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state)
if type(other) is And:
other = cast(And, other)
return And(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules),
_simplification_state=self.simplification_state.merge(other.simplification_state))

View File

@@ -65,7 +65,7 @@ class TestBooksanityNone(SVTestBase):
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
class TestBooksanityPowers(SVTestBase):
@@ -111,7 +111,7 @@ class TestBooksanityPowers(SVTestBase):
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
class TestBooksanityPowersAndSkills(SVTestBase):
@@ -157,7 +157,7 @@ class TestBooksanityPowersAndSkills(SVTestBase):
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
class TestBooksanityAll(SVTestBase):
@@ -203,4 +203,4 @@ class TestBooksanityAll(SVTestBase):
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)

View File

@@ -53,8 +53,6 @@ class TestDifferentSettings(SVTestCase):
def test_money_rule_caching(self):
options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
StartingMoney.internal_name: 5000}
options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
StartingMoney.internal_name: 5000}
multiplayer_options = [options_festivals_limited_money, options_festivals_limited_money]
multiworld = setup_multiworld(multiplayer_options)

View File

@@ -25,7 +25,7 @@ class TestWalnutsanityNone(SVTestBase):
self.collect("Island Obelisk")
self.collect("Island West Turtle")
self.collect("Progressive House")
items = self.collect("5 Golden Walnuts", 10)
self.collect("5 Golden Walnuts", 10)
self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
self.collect("Island North Turtle")
@@ -81,10 +81,10 @@ class TestWalnutsanityPuzzles(SVTestBase):
self.collect("Combat Level", 10)
self.collect("Mining Level", 10)
for location in locations:
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.collect("Open Professor Snail Cave")
for location in locations:
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
class TestWalnutsanityBushes(SVTestBase):
@@ -126,10 +126,10 @@ class TestWalnutsanityPuzzlesAndBushes(SVTestBase):
# You need to receive 25, and collect 15
self.collect("Island Obelisk")
self.collect("Island West Turtle")
items = self.collect("5 Golden Walnuts", 5)
self.collect("5 Golden Walnuts", 5)
self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
items = self.collect("Island North Turtle")
self.collect("Island North Turtle")
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
@@ -203,7 +203,7 @@ class TestWalnutsanityAll(SVTestBase):
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
self.remove(items)
self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
items = self.collect("5 Golden Walnuts", 4)
items = self.collect("3 Golden Walnuts", 6)
items = self.collect("Golden Walnut", 2)
self.collect("5 Golden Walnuts", 4)
self.collect("3 Golden Walnuts", 6)
self.collect("Golden Walnut", 2)
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))

View File

@@ -1,7 +1,7 @@
from typing import List
from unittest import TestCase
from BaseClasses import CollectionState, Location
from BaseClasses import CollectionState, Location, Region
from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach
from ...stardew_rule.rule_explain import explain
@@ -40,19 +40,42 @@ class RuleAssertMixin(TestCase):
raise AssertionError(f"Error while checking rule {rule}: {e}"
f"\nExplanation: {expl}")
def assert_reach_location_true(self, location: Location, state: CollectionState):
expl = explain(Reach(location.name, "Location", 1), state)
def assert_can_reach_location(self, location: Location | str, state: CollectionState) -> None:
location_name = location.name if isinstance(location, Location) else location
expl = explain(Reach(location_name, "Location", 1), state)
try:
can_reach = location.can_reach(state)
can_reach = state.can_reach_location(location_name, 1)
self.assertTrue(can_reach, expl)
except KeyError as e:
raise AssertionError(f"Error while checking location {location.name}: {e}"
raise AssertionError(f"Error while checking location {location_name}: {e}"
f"\nExplanation: {expl}")
def assert_reach_location_false(self, location: Location, state: CollectionState):
expl = explain(Reach(location.name, "Location", 1), state, expected=False)
def assert_cannot_reach_location(self, location: Location | str, state: CollectionState) -> None:
location_name = location.name if isinstance(location, Location) else location
expl = explain(Reach(location_name, "Location", 1), state, expected=False)
try:
self.assertFalse(location.can_reach(state), expl)
can_reach = state.can_reach_location(location_name, 1)
self.assertFalse(can_reach, expl)
except KeyError as e:
raise AssertionError(f"Error while checking location {location.name}: {e}"
raise AssertionError(f"Error while checking location {location_name}: {e}"
f"\nExplanation: {expl}")
def assert_can_reach_region(self, region: Region | str, state: CollectionState) -> None:
region_name = region.name if isinstance(region, Region) else region
expl = explain(Reach(region_name, "Region", 1), state)
try:
can_reach = state.can_reach_region(region_name, 1)
self.assertTrue(can_reach, expl)
except KeyError as e:
raise AssertionError(f"Error while checking region {region_name}: {e}"
f"\nExplanation: {expl}")
def assert_cannot_reach_region(self, region: Region | str, state: CollectionState) -> None:
region_name = region.name if isinstance(region, Region) else region
expl = explain(Reach(region_name, "Region", 1), state, expected=False)
try:
can_reach = state.can_reach_region(region_name, 1)
self.assertFalse(can_reach, expl)
except KeyError as e:
raise AssertionError(f"Error while checking region {region_name}: {e}"
f"\nExplanation: {expl}")

View File

@@ -53,7 +53,7 @@ class WorldAssertMixin(RuleAssertMixin, TestCase):
def assert_can_reach_everything(self, multiworld: MultiWorld):
for location in multiworld.get_locations():
self.assert_reach_location_true(location, multiworld.state)
self.assert_can_reach_location(location, multiworld.state)
def assert_basic_checks(self, multiworld: MultiWorld):
self.assert_same_number_items_locations(multiworld)

View File

@@ -1,13 +1,13 @@
import random
from BaseClasses import get_seed
from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, \
fill_dataclass_with_default
from .. import SVTestBase, SVTestCase, allsanity_mods_6_x_x, fill_dataclass_with_default
from ..assertion import ModAssertMixin, WorldAssertMixin
from ... import items, Group, ItemClassification, create_content
from ... import options
from ...items import items_by_group
from ...mods.mod_data import ModNames
from ...options import SkillProgression, Walnutsanity
from ...options.options import all_mods
from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
@@ -20,17 +20,58 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
self.assert_basic_checks(multi_world)
self.assert_stray_mod_items(mod, multi_world)
def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self):
for option in options.EntranceRandomization.options:
for mod in options.Mods.valid_keys:
world_options = {
options.EntranceRandomization: options.EntranceRandomization.options[option],
options.Mods: mod,
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false
}
with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)
self.assert_stray_mod_items(mod, multi_world)
# The following tests validate that ER still generates winnable and logically-sane games with given mods.
# Mods that do not interact with entrances are skipped
# Not all ER settings are tested, because 'buildings' is, essentially, a superset of all others
def test_deepwoods_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.deepwoods, options.EntranceRandomization.option_buildings)
def test_juna_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.juna, options.EntranceRandomization.option_buildings)
def test_jasper_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.jasper, options.EntranceRandomization.option_buildings)
def test_alec_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.alec, options.EntranceRandomization.option_buildings)
def test_yoba_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.yoba, options.EntranceRandomization.option_buildings)
def test_eugene_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.eugene, options.EntranceRandomization.option_buildings)
def test_ayeisha_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.ayeisha, options.EntranceRandomization.option_buildings)
def test_riley_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.riley, options.EntranceRandomization.option_buildings)
def test_sve_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.sve, options.EntranceRandomization.option_buildings)
def test_alecto_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.alecto, options.EntranceRandomization.option_buildings)
def test_lacey_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.lacey, options.EntranceRandomization.option_buildings)
def test_boarding_house_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.boarding_house, options.EntranceRandomization.option_buildings)
def test_all_mods_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(all_mods, options.EntranceRandomization.option_buildings)
def perform_basic_checks_on_mod_with_er(self, mods: str | set[str], er_option: int) -> None:
if isinstance(mods, str):
mods = {mods}
world_options = {
options.EntranceRandomization: er_option,
options.Mods: frozenset(mods),
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false
}
with self.solo_world_sub_test(f"entrance_randomization: {er_option}, Mods: {mods}", world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)
def test_allsanity_all_mods_when_generate_then_basic_checks(self):
with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _):
@@ -147,19 +188,3 @@ class TestModEntranceRando(SVTestCase):
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization.")
class TestModTraps(SVTestCase):
def test_given_traps_when_generate_then_all_traps_in_pool(self):
for value in options.TrapItems.options:
if value == "no_traps":
continue
world_options = allsanity_no_mods_6_x_x()
world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"})
with solo_multiworld(world_options) as (multi_world, _):
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
multiworld_items = [item.name for item in multi_world.get_items()]
for item in trap_items:
with self.subTest(f"Option: {value}, Item: {item}"):
self.assertIn(item, multiworld_items)

View File

@@ -0,0 +1,28 @@
from .. import SVTestBase
from ... import options
class TestNoGingerIslandCraftingRecipesAreRequired(SVTestBase):
options = {
options.Goal.internal_name: options.Goal.option_craft_master,
options.Craftsanity.internal_name: options.Craftsanity.option_all,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
options.Mods.internal_name: frozenset(options.Mods.valid_keys)
}
@property
def run_default_tests(self) -> bool:
return True
class TestNoGingerIslandCookingRecipesAreRequired(SVTestBase):
options = {
options.Goal.internal_name: options.Goal.option_gourmet_chef,
options.Cooksanity.internal_name: options.Cooksanity.option_all,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
options.Mods.internal_name: frozenset(options.Mods.valid_keys)
}
@property
def run_default_tests(self) -> bool:
return True

View File

@@ -12,15 +12,13 @@ class TestBooksLogic(SVTestBase):
location = self.multiworld.get_location("Read Mapping Cave Systems", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.collect("Progressive Weapon")
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)

View File

@@ -1,5 +1,4 @@
from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \
ElevatorProgression, SpecialOrderLocations
from ...options import SeasonRandomization, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, ElevatorProgression, SpecialOrderLocations
from ...strings.fish_names import Fish
from ...test import SVTestBase
@@ -44,18 +43,18 @@ class TestNeedRegionToCatchFish(SVTestBase):
self.collect_all_the_money()
item_names = fish_and_items[fish]
location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
items = []
for item_name in item_names:
items.append(self.collect(item_name))
with self.subTest(f"{fish} can be reached with {item_names}"):
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
for item_required in items:
self.multiworld.state = self.original_state.copy()
with self.subTest(f"{fish} requires {item_required.name}"):
for item_to_collect in items:
if item_to_collect.name != item_required.name:
self.collect(item_to_collect)
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.multiworld.state = self.original_state.copy()

View File

@@ -39,10 +39,10 @@ class TestSkillProgressionProgressive(SVTestBase):
with self.subTest(location_name):
if level > 1:
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.collect(f"{skill} Level")
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
self.reset_collection_state()
@@ -88,7 +88,7 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
for skill in all_vanilla_skills:
with self.subTest(skill):
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
self.assert_reach_location_true(location, self.multiworld.state)
self.assert_can_reach_location(location, self.multiworld.state)
self.reset_collection_state()
@@ -99,7 +99,7 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
self.remove_one_by_name(f"{skill} Level")
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.reset_collection_state()
@@ -108,6 +108,6 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
self.remove_one_by_name(f"Progressive Pickaxe")
location = self.multiworld.get_location("Mining Mastery", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.reset_collection_state()

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
@@ -78,7 +78,8 @@ class TunicWorld(World):
settings: ClassVar[TunicSettings]
item_name_groups = item_name_groups
location_name_groups = location_name_groups
location_name_groups.update(grass_location_name_groups)
for group_name, members in grass_location_name_groups.items():
location_name_groups.setdefault(group_name, set()).update(members)
item_name_to_id = item_name_to_id
location_name_to_id = standard_location_name_to_id.copy()
@@ -241,10 +242,18 @@ class TunicWorld(World):
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
item_data = item_table[name]
# if item_data.combat_ic is None, it'll take item_data.classification instead
itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None)
# evaluate alternate classifications based on options
# it'll choose whichever classification isn't None first in this if else tree
itemclass: ItemClassification = (classification
or (item_data.combat_ic if self.options.combat_logic else None)
or (ItemClassification.progression | ItemClassification.useful
if name == "Glass Cannon" and self.options.grass_randomizer
and not self.options.start_with_sword else None)
or (ItemClassification.progression | ItemClassification.useful
if name == "Shield" and self.options.ladder_storage
and not self.options.ladder_storage_without_items else None)
or item_data.classification)
return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player)
return TunicItem(name, itemclass, self.item_name_to_id[name], self.player)
def create_items(self) -> None:
tunic_items: List[TunicItem] = []
@@ -277,8 +286,6 @@ class TunicWorld(World):
if self.options.grass_randomizer:
items_to_create["Grass"] = len(grass_location_table)
tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression))
items_to_create["Glass Cannon"] = 0
for grass_location in excluded_grass_locations:
self.get_location(grass_location).place_locked_item(self.create_item("Grass"))
items_to_create["Grass"] -= len(excluded_grass_locations)
@@ -331,10 +338,11 @@ class TunicWorld(World):
remove_filler(items_to_create[gold_hexagon])
# Sort for deterministic order
for hero_relic in sorted(item_name_groups["Hero Relics"]):
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
items_to_create[hero_relic] = 0
if not self.options.combat_logic:
# Sort for deterministic order
for hero_relic in sorted(item_name_groups["Hero Relics"]):
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
items_to_create[hero_relic] = 0
if not self.options.ability_shuffling:
# Sort for deterministic order
@@ -349,11 +357,6 @@ class TunicWorld(World):
tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful))
items_to_create[page] = 0
# logically relevant if you have ladder storage enabled
if self.options.ladder_storage and not self.options.ladder_storage_without_items:
tunic_items.append(self.create_item("Shield", ItemClassification.progression))
items_to_create["Shield"] = 0
if self.options.maskless:
tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful))
items_to_create["Scavenger Mask"] = 0
@@ -411,7 +414,7 @@ class TunicWorld(World):
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
if world.options.local_fill.value > 0]
if tunic_fill_worlds:
if tunic_fill_worlds and multiworld.players > 1:
grass_fill: List[TunicItem] = []
non_grass_fill: List[TunicItem] = []
grass_fill_locations: List[Location] = []
@@ -500,6 +503,13 @@ class TunicWorld(World):
state.tunic_need_to_reset_combat_from_remove[self.player] = True
return change
def write_spoiler_header(self, spoiler_handle: TextIO):
if self.options.hexagon_quest and self.options.ability_shuffling:
spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n")
for ability in self.ability_unlocks:
# Remove parentheses for better readability
spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n')
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando:
hint_data.update({self.player: {}})

View File

@@ -8,6 +8,7 @@ from worlds.AutoWorld import LogicMixin
# the vanilla stats you are expected to have to get through an area, based on where they are in vanilla
class AreaStats(NamedTuple):
"""Attack, Defense, Potion, HP, SP, MP, Flasks, Equipment, is_boss"""
att_level: int
def_level: int
potion_level: int # all 3 are before your first bonfire after getting the upgrade page, third costs 1k
@@ -41,7 +42,7 @@ area_data: Dict[str, AreaStats] = {
"Rooted Ziggurat": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"]),
"Boss Scavenger": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"], is_boss=True),
"Swamp": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
"Cathedral": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
# Cathedral has the same requirements as Swamp
# marked as boss because the garden knights can't get hurt by stick
"Gauntlet": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"], is_boss=True),
"The Heir": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic", "Laurels"], is_boss=True),
@@ -49,8 +50,10 @@ area_data: Dict[str, AreaStats] = {
# these are used for caching which areas can currently be reached in state
# Gauntlet does not have exclusively higher stat requirements, so it will be checked separately
boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"]
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss]
# Swamp does not have exclusively higher stat requirements, so it will be checked separately
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"]
class CombatState(IntEnum):
@@ -89,6 +92,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool
elif area_name in non_boss_areas:
area_list = non_boss_areas
else:
# this is to check Swamp and Gauntlet on their own
area_list = [area_name]
if met_combat_reqs:
@@ -114,88 +118,99 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
extra_att_needed = 0
extra_def_needed = 0
extra_mp_needed = 0
has_magic = state.has_any({"Magic Wand", "Gun"}, player)
stick_bool = False
sword_bool = False
has_magic = state.has_any(("Magic Wand", "Gun"), player)
sword_bool = has_sword(state, player)
stick_bool = sword_bool or has_melee(state, player)
equipment = data.equipment.copy()
for item in data.equipment:
if item == "Stick":
if not has_melee(state, player):
if not stick_bool:
if has_magic:
equipment.remove("Stick")
if "Magic" not in equipment:
equipment.append("Magic")
# magic can make up for the lack of stick
extra_mp_needed += 2
extra_att_needed -= 16
extra_att_needed -= 32
else:
return False
else:
stick_bool = True
elif item == "Sword":
if not has_sword(state, player):
if not sword_bool:
# need sword for bosses
if data.is_boss:
return False
equipment.remove("Sword")
if has_magic:
if "Magic" not in equipment:
equipment.append("Magic")
# +4 mp pretty much makes up for the lack of sword, at least in Quarry
extra_mp_needed += 4
# stick is a backup plan, and doesn't scale well, so let's require a little less
extra_att_needed -= 2
elif has_melee(state, player):
if stick_bool:
# stick is a backup plan, and doesn't scale well, so let's require a little less
equipment.append("Stick")
extra_att_needed -= 2
else:
extra_mp_needed += 2
extra_att_needed -= 32
elif stick_bool:
equipment.append("Stick")
# may revise this later based on feedback
extra_att_needed += 3
extra_def_needed += 2
else:
return False
else:
sword_bool = True
# just increase the stat requirement, we'll check for shield when calculating defense
elif item == "Shield":
if not state.has("Shield", player):
extra_def_needed += 2
equipment.remove("Shield")
extra_def_needed += 2
elif item == "Laurels":
if not state.has("Hero's Laurels", player):
# these are entirely based on vibes
extra_att_needed += 2
extra_def_needed += 3
# require Laurels for the Heir
return False
elif item == "Magic":
if not has_magic:
equipment.remove("Magic")
extra_att_needed += 2
extra_def_needed += 2
extra_mp_needed -= 16
extra_mp_needed -= 32
modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level,
data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count)
if not has_required_stats(modified_stats, state, player):
data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count,
equipment, data.is_boss)
if has_required_stats(modified_stats, state, player):
return True
else:
# we may need to check if you would have the required stats if you were missing a weapon
# it's kinda janky, but these only get hit in less than once per 100 generations, so whatever
if sword_bool and "Sword" in data.equipment and "Magic" in data.equipment:
# we need to check if you would have the required stats if you didn't have melee
equip_list = [item for item in data.equipment if item != "Sword"]
more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level,
data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count,
equip_list)
if sword_bool and "Sword" in equipment and has_magic:
# we need to check if you would have the required stats if you didn't have the sword
equip_list = [item for item in equipment if item != "Sword"]
if "Magic" not in equip_list:
equip_list.append("Magic")
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
modified_stats.potion_level, modified_stats.hp_level,
modified_stats.sp_level, modified_stats.mp_level + 4,
modified_stats.potion_count, equip_list, data.is_boss)
if check_combat_reqs("none", state, player, more_modified_stats):
return True
# and we need to check if you would have the required stats if you didn't have magic
equip_list = [item for item in data.equipment if item != "Magic"]
more_modified_stats = AreaStats(data.att_level + 2, data.def_level + 2, data.potion_level,
data.hp_level, data.sp_level, data.mp_level - 16, data.potion_count,
equip_list)
if check_combat_reqs("none", state, player, more_modified_stats):
return True
return False
elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment:
elif stick_bool and "Stick" in equipment and has_magic:
# we need to check if you would have the required stats if you didn't have the stick
equip_list = [item for item in data.equipment if item != "Stick"]
more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level,
data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count,
equip_list)
equip_list = [item for item in equipment if item != "Stick"]
if "Magic" not in equip_list:
equip_list.append("Magic")
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
modified_stats.potion_level, modified_stats.hp_level,
modified_stats.sp_level, modified_stats.mp_level + 4,
modified_stats.potion_count, equip_list, data.is_boss)
if check_combat_reqs("none", state, player, more_modified_stats):
return True
return False
else:
return False
return True
return False
# check if you have the required stats, and the money to afford them
@@ -203,72 +218,63 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
# but that's fine -- it's already pretty generous to begin with
def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> bool:
money_required = 0
player_att = 0
att_required = data.att_level
player_att, att_offerings = get_att_level(state, player)
# check if we actually need the stat before checking state
if data.att_level > 1:
player_att, att_offerings = get_att_level(state, player)
if player_att < data.att_level:
return False
# if you have 2 more attack than needed, we can forego needing mp
if data.mp_level > 1:
if player_att < data.att_level + 2:
player_mp, mp_offerings = get_mp_level(state, player)
if player_mp < data.mp_level:
return False
else:
extra_mp = player_mp - data.mp_level
paid_mp = max(0, mp_offerings - extra_mp)
# mp costs 300 for the first, +50 for each additional
money_per_mp = 300
for _ in range(paid_mp):
money_required += money_per_mp
money_per_mp += 50
else:
extra_att = player_att - data.att_level
paid_att = max(0, att_offerings - extra_att)
# attack upgrades cost 100 for the first, +50 for each additional
money_per_att = 100
for _ in range(paid_att):
money_required += money_per_att
money_per_att += 50
att_required += 2
if player_att < att_required:
return False
else:
extra_att = player_att - att_required
paid_att = max(0, att_offerings - extra_att)
# attack upgrades cost 100 for the first, +50 for each additional
money_per_att = 100
for _ in range(paid_att):
money_required += money_per_att
money_per_att += 50
# adding defense and sp together since they accomplish similar things: making you take less damage
if data.def_level + data.sp_level > 2:
player_def, def_offerings = get_def_level(state, player)
player_sp, sp_offerings = get_sp_level(state, player)
if player_def + player_sp < data.def_level + data.sp_level:
req_stats = data.def_level + data.sp_level
if player_def + player_sp < req_stats:
return False
else:
free_def = player_def - def_offerings
free_sp = player_sp - sp_offerings
paid_stats = data.def_level + data.sp_level - free_def - free_sp
sp_to_buy = 0
if paid_stats <= 0:
# if you don't have to pay for any stats, you don't need money for these upgrades
def_to_buy = 0
elif paid_stats <= def_offerings:
# get the amount needed to buy these def offerings
def_to_buy = paid_stats
if free_sp + free_def >= req_stats:
# you don't need to buy upgrades
pass
else:
def_to_buy = def_offerings
sp_to_buy = max(0, paid_stats - def_offerings)
# if you have to buy more than 3 def, it's cheaper to buy 1 extra sp
if def_to_buy > 3 and sp_offerings > 0:
def_to_buy -= 1
sp_to_buy += 1
# def costs 100 for the first, +50 for each additional
money_per_def = 100
for _ in range(def_to_buy):
money_required += money_per_def
money_per_def += 50
# sp costs 200 for the first, +200 for each additional
money_per_sp = 200
for _ in range(sp_to_buy):
money_required += money_per_sp
money_per_sp += 200
# if you have 2 more attack than needed, we can forego needing mp
if data.mp_level > 1 and player_att < data.att_level + 2:
player_mp, mp_offerings = get_mp_level(state, player)
if player_mp < data.mp_level:
return False
else:
extra_mp = player_mp - data.mp_level
paid_mp = max(0, mp_offerings - extra_mp)
# mp costs 300 for the first, +50 for each additional
money_per_mp = 300
for _ in range(paid_mp):
money_required += money_per_mp
money_per_mp += 50
# we need to pick the cheapest option that gets us above the stats we need
# first number is def, second number is sp
upgrade_options: set[tuple[int, int]] = set()
stats_to_buy = req_stats - free_def - free_sp
for paid_def in range(0, min(def_offerings + 1, stats_to_buy + 1)):
sp_required = stats_to_buy - paid_def
if sp_offerings >= sp_required:
if sp_required < 0:
break
upgrade_options.add((paid_def, stats_to_buy - paid_def))
costs = [calc_def_sp_cost(defense, sp) for defense, sp in upgrade_options]
money_required += min(costs)
req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count)
player_potion, potion_offerings = get_potion_level(state, player)
@@ -279,53 +285,30 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) ->
return False
else:
# need a way to determine which of potion offerings or hp offerings you can reduce
# your level if you didn't pay for offerings
free_potion = player_potion - potion_offerings
free_hp = player_hp - hp_offerings
paid_hp_count = 0
paid_potion_count = 0
if calc_effective_hp(free_hp, free_potion, player_potion_count) >= req_effective_hp:
# you don't need to buy upgrades
pass
# if you have no potions, or no potion upgrades, you only need to check your hp upgrades
elif player_potion_count == 0 or potion_offerings == 0:
# check if you have enough hp at each paid hp offering
for i in range(hp_offerings):
paid_hp_count = i + 1
if calc_effective_hp(paid_hp_count, 0, player_potion_count) > req_effective_hp:
break
else:
for i in range(potion_offerings):
paid_potion_count = i + 1
if calc_effective_hp(free_hp, free_potion + paid_potion_count, player_potion_count) > req_effective_hp:
break
for j in range(hp_offerings):
paid_hp_count = j + 1
if (calc_effective_hp(free_hp + paid_hp_count, free_potion + paid_potion_count, player_potion_count)
> req_effective_hp):
# we need to pick the cheapest option that gets us above the amount of effective HP we need
# first number is hp, second number is potion
upgrade_options: set[tuple[int, int]] = set()
# filter out exclusively worse options
lowest_hp_added = hp_offerings + 1
for paid_potion in range(0, potion_offerings + 1):
# check quantities of hp offerings for each potion offering
for paid_hp in range(0, lowest_hp_added):
if (calc_effective_hp(free_hp + paid_hp, free_potion + paid_potion, player_potion_count)
>= req_effective_hp):
upgrade_options.add((paid_hp, paid_potion))
lowest_hp_added = paid_hp
break
# hp costs 200 for the first, +50 for each additional
money_per_hp = 200
for _ in range(paid_hp_count):
money_required += money_per_hp
money_per_hp += 50
# potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional
# currently we assume you will not buy past the second potion upgrade, but we might change our minds later
money_per_potion = 100
for _ in range(paid_potion_count):
money_required += money_per_potion
if money_per_potion == 100:
money_per_potion = 300
elif money_per_potion == 300:
money_per_potion = 1000
else:
money_per_potion += 200
costs = [calc_hp_potion_cost(hp, potion) for hp, potion in upgrade_options]
money_required += min(costs)
if money_required > get_money_count(state, player):
return False
return True
return get_money_count(state, player) >= money_required
# returns a tuple of your max attack level, the number of attack offerings
@@ -336,7 +319,8 @@ def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]:
if sword_level >= 3:
att_upgrades += min(2, sword_level - 2)
# attack falls off, can just cap it at 8 for simplicity
return min(8, 1 + att_offerings + att_upgrades), att_offerings
return (min(8, 1 + att_offerings + att_upgrades)
+ (1 if state.has("Hero's Laurels", player) else 0), att_offerings)
# returns a tuple of your max defense level, the number of defense offerings
@@ -344,7 +328,9 @@ def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]:
def_offerings = state.count("DEF Offering", player)
# defense falls off, can just cap it at 8 for simplicity
return (min(8, 1 + def_offerings
+ state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)),
+ state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player))
+ (2 if state.has("Shield", player) else 0)
+ (2 if state.has("Hero's Laurels", player) else 0),
def_offerings)
@@ -408,6 +394,46 @@ def get_money_count(state: CollectionState, player: int) -> int:
return money
def calc_hp_potion_cost(hp_upgrades: int, potion_upgrades: int) -> int:
money = 0
# hp costs 200 for the first, +50 for each additional
money_per_hp = 200
for _ in range(hp_upgrades):
money += money_per_hp
money_per_hp += 50
# potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional
# currently we assume you will not buy past the second potion upgrade, but we might change our minds later
money_per_potion = 100
for _ in range(potion_upgrades):
money += money_per_potion
if money_per_potion == 100:
money_per_potion = 300
elif money_per_potion == 300:
money_per_potion = 1000
else:
money_per_potion += 200
return money
def calc_def_sp_cost(def_upgrades: int, sp_upgrades: int) -> int:
money = 0
money_per_def = 100
for _ in range(def_upgrades):
money += money_per_def
money_per_def += 50
money_per_sp = 200
for _ in range(sp_upgrades):
money += money_per_sp
money_per_sp += 200
return money
class TunicState(LogicMixin):
tunic_need_to_reset_combat_from_collect: Dict[int, bool]
tunic_need_to_reset_combat_from_remove: Dict[int, bool]
@@ -420,3 +446,5 @@ class TunicState(LogicMixin):
self.tunic_need_to_reset_combat_from_remove = defaultdict(lambda: False)
# the per-player, per-area state of combat checking -- unchecked, failed, or succeeded
self.tunic_area_combat_state = defaultdict(lambda: defaultdict(lambda: CombatState.unchecked))
# a copy_mixin was intentionally excluded because the empty state from init_mixin
# will always be appropriate for recalculating the logic cache

View File

@@ -1386,9 +1386,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# need to fight through the rudelings and turret, or just laurels from near the windmill
set_rule(ow_to_well_entry,
lambda state: state.has(laurels, player)
or has_combat_reqs("East Forest", state, player))
or has_combat_reqs("Before Well", state, player))
set_rule(ow_tunnel_beach,
lambda state: has_combat_reqs("East Forest", state, player))
lambda state: has_combat_reqs("Before Well", state, player))
add_rule(atoll_statue,
lambda state: has_combat_reqs("Ruined Atoll", state, player))
@@ -1467,12 +1467,12 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
set_rule(cath_entry_to_elev,
lambda state: options.entrance_rando
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
or (has_ability(prayer, state, world) and has_combat_reqs("Cathedral", state, player)))
or (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player)))
set_rule(cath_entry_to_main,
lambda state: has_combat_reqs("Cathedral", state, player))
lambda state: has_combat_reqs("Swamp", state, player))
set_rule(cath_elev_to_main,
lambda state: has_combat_reqs("Cathedral", state, player))
lambda state: has_combat_reqs("Swamp", state, player))
# for spots where you can go into and come out of an entrance to reset enemy aggro
if world.options.entrance_rando:
@@ -1835,10 +1835,10 @@ def set_er_location_rules(world: "TunicWorld") -> None:
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld")
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "Before Well", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "Before Well", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "Before Well", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "Before Well", dagger=True)
combat_logic_to_loc("Overworld - [Northwest] Chest Near Turret", "Before Well")
add_rule(world.get_location("Hourglass Cave - Hourglass Chest"),
@@ -1927,4 +1927,4 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# zip through the rubble to sneakily grab this chest, or just fight to it
add_rule(world.get_location("Cathedral - [1F] Near Spikes"),
lambda state: laurels_zip(state, world) or has_combat_reqs("Cathedral", state, player))
lambda state: laurels_zip(state, world) or has_combat_reqs("Swamp", state, player))

Some files were not shown because too many files have changed in this diff Show More