Compare commits

..

49 Commits

Author SHA1 Message Date
NewSoupVi
8d04239504 use url_for with _anchor (#3279) 2024-05-08 08:34:17 +02:00
NewSoupVi
59a000033e Merge branch 'main' into NewSoupVi-patch-1 2024-05-07 12:47:50 +02:00
Bryce Wilson
76962b8b3b Pokemon Emerald: Fix incorrect access to slateport water encounters (#3243) 2024-05-07 12:43:35 +02:00
NewSoupVi
e04db57dce Core: Add has_list and count_list and improve (?) count_group (#2934)
* Add has_list and count_list and improve (?) count_group

* MESSENGER STOP

* Add docstrings to has_list and count_list

* Add docstrings for has_group and count_group as well

* oops

* Rename to has_from

* docstrings improvement again

* Docstring
2024-05-07 09:23:25 +02:00
digiholic
12b8fef1aa Adds a canary byte check before sending game completion (#3217) 2024-05-07 09:22:11 +02:00
NewSoupVi
0ac8844f6f Core: Add "has_all_counts" and "has_any_count" functions to CollectionState (#2933)
* Add has_all_counts and has_any_counts

* Messenger gave me a red x and I'm mad about it

* Update BaseClasses.py

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

* Update BaseClasses.py

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

* Mapping instead of Dict

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-07 08:15:09 +02:00
black-sliver
23eca7d747 SoE: Docs: rework some styling (#3268)
* Docs: SoE: move header and fix header level

* Docs: SoE: be pedantic for required software
2024-05-06 10:55:25 +02:00
Alchav
1a563a14fc LTTP: Yet more LTTP logic fixes (#3270) 2024-05-06 09:36:08 +02:00
jamesbrq
5935093615 Mario & Luigi: Superstar Saga: Implement New Game (#2754)
* Commit for PR

* Commit for PR

* Update worlds/mlss/Client.py

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/mlss/__init__.py

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/mlss/__init__.py

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/mlss/docs/setup_en.md

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Remove deprecated import. Updated settings and romfile syntax

* Updated Options to new system. Changed all references from MultiWorld to World

* Changed switch statements to if else

* Update en_Mario & Luigi Superstar Saga.md

* Updated client.py

* Update Client.py

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Updated logic, Updated patch implementation, Removed unused imports, Cleaned up Code

* Update __init__.py

* Changed reference from world to mlssworld

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Fix merge conflict + update prep

* v1.2

* Leftover print commands

* Update basepatch.bsdiff

* Update basepatch.bsdiff

* v1.3

* Update Rom.py

* Change tracker locations to serverside, no longer locations. Various code cleanup and logic changes.

* Event removal continuation.

* Partial Implementation of APPP (Incomplete))

* v1.4 Implemented APPP

* Docs Updated

* Update Rom.py

* Update setup_en.md

* Update Rom.py

* Update Rules.py

* Fix for APPP being broken on webhost

* Update Rom.py

* Update Rom.py

* Location name fixes + pants color fixes

* Update Rules.py

* Fix for ultra hammer cutscene

* Fixed compat. issues with python ver. 3.8

* Updated hidden block yaml option

* pre-v1.5

* Update Client.py

* Update basepatch.bsdiff

* v1.5

* Update XP multiplier to have a minimum of 0

* Update 'Beanfruit' to 'Bean Fruit'

* v1.6

* Update Rom.py

* Update basepatch.bsdiff

* Initial review refactor

* Revert state logic changes. Continuation of refactor.

* Fixed failed generations. Finished refactor.

* Reworked colors. Removed all .txt files

* Actually removed the .txt files this time

* Update Rom.py

* Update README.md

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

* Update worlds/mlss/Options.py

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

* Update worlds/mlss/Client.py

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/__init__.py

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/Data.py

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

* Review refactor.

* Update README.md

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

* Update worlds/mlss/Rules.py

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

* Add coin blocks to LocationName

* Refactor.

* Update Items.py

* Delete mlss.apworld

* Small asm bugfix

* Update basepatch.bsdiff

* Client sends less messages to server

* Update basepatch.bsdiff

---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-06 09:15:06 +02:00
Fabian Dill
2aa3ef372d WebHost: use redirect for /room form submission (#3271) 2024-05-05 22:59:51 +02:00
Bryce Wilson
d94cf8dcb2 Pokemon Emerald: Add event ticket locations to client data store flags (#3177)
* Pokemon Emerald: Add event ticket locations to client data store flags

* Pokemon Emerald: Add regi doors event flag

* Pokemon Emerald: Add more tracker flags
2024-05-05 10:46:11 +02:00
PoryGone
5fae1c087e Celeste 64: v1.2 Content Update (#3210)
* Cleanup and new option support

* Handle new locations

* Support higher Strawberry counts

* Don't add start inventory items to the pool

* Support Move Shuffle functionality and items

* Hard and Move Shuffle Logic

* Fix Options

* Update CHANGELOG.md

* Add standard moves logic for signs 3 and 4

* Fix Option Tooltip

* Add tracker link to setup guide

* Fix unit test

* Fix option tooltips

* Missing Space

* Move option checking out of rule function

* Delete just_gen500.bat
2024-05-05 08:58:49 +02:00
NewSoupVi
baca95d49d WebHost: Fix setup guide link not working for games with special characters 2024-05-05 08:20:45 +02:00
Bryce Wilson
7e61211365 Pokemon Emerald: Convert to procedure patch (#2995)
* Pokemon Emerald: Convert to procedure patch

* Pokemon Emerald: Remove assertion for vanilla rom's existence

* Pokemon Emerald: Add APPP implication to changelog

* Pokemon Emerald: Move procedure patch changelog line to new version

* Pokemon Emerald: Modify changelog versions

* Pokemon Emerald: Fix patch file download not appearing

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-05 07:08:24 +02:00
Bryce Wilson
7603b4a88f Pokemon Emerald: Change dexsanity to not create locations for blacklisted wilds (#3056) 2024-05-04 21:44:38 +02:00
Aaron Wagener
005fc4e864 Fill: allow for single player fill restrictive placement and sweeping (#2415)
* Core: allow for single player state sweeping

* Fill: have distribute items use single_player fill when it can

* oop

* pass locations to sweep_for_events instead of the player

* finally found the diff that was breaking swap

* LTTP fills everyone's dungeons at once, not just a single player's

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-04 12:42:36 +02:00
Star Rauchenberger
28262a31b8 Lingo: Started using OptionError (#3251) 2024-05-04 08:40:17 +02:00
Bryce Wilson
660b068f5a Pokemon Emerald: Use OptionError (#3264) 2024-05-04 08:38:24 +02:00
Thorsten Horberth
879c3407d8 Yoshi's World: Fixed minor logic inconsistincy in Rules.py (#3241)
* Fixed Logic in Rules.py

As of easy logic of this goal is
    set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state)))
normal logic shouldn't need any collectable.

* Corrected Logic Rules.py
2024-05-04 04:29:12 +02:00
Scipio Wright
d5683c4326 Core: Make output when hinting something with multiple copies show up in a better order (#3245)
* Make the hint info show up in a better order

* Change how old_hints is modified/done

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-04 04:28:09 +02:00
Fabian Dill
f27d1d635b SNIClient: restore old operands header (#3242) 2024-05-03 22:00:05 +02:00
Nicholas Saylor
298c9fc159 Fixed typo and odd capitalization (#3233) 2024-05-03 12:23:08 +02:00
Scipio Wright
26188230b7 TUNIC: Better seed groups for Entrance Rando (#2998)
* Update entrance rando description to discuss seed groups

* Starting off, setting up some names

* It lives

* Some preliminary plando connection handling, probably has errors

* Add missed comma

* if -> elif

* I think this is working properly to handle plando connections

* Update comments

* Fix up shop -> shop portal stuff

* Add back comma that got removed for no reason in the ladder PR

* Remove unnecessary if else

* add back the actually necessary if but not the else

* okay they were both necessary

* Update entrance rando description

* blasphemy

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>

* Rename other instances of tunc -> tunic

* Update per Vi's review (thank you)

* Fix a not that shouldn't have been

* Rearrange, update per Vi's comments (thank you)

* Fix indent

* Add a .value

* Add .values

* Fix bad comparison

* Add a not that was supposed to be there

* Replace another isinstance

* Revise option description

* Fix per Kaito's comment

Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>

---------

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>
2024-05-03 07:21:27 +02:00
t3hf1gm3nt
b68be7360c [TLOZ]: Remove use of per_slot_randoms (#3255)
We only used it in two spots for randomizing the secret rupee cave values. Uses proper world random now.
2024-05-03 02:56:20 +02:00
t3hf1gm3nt
255e52642e TLOZ: Fix rings classification, so they are actually considered for logic (#3253) 2024-05-02 16:49:39 -05:00
qwint
49862dca1f move godhome events to create_regions with the others to not try and make them non-events when unshuffled is on (#3221) 2024-05-02 15:26:17 +02:00
Star Rauchenberger
0d586a4467 Lingo: Fix broken good item in panelsanity (#3249) 2024-05-02 15:14:30 +02:00
Ziktofel
8c8b29ae92 SC2: For non-campaign order pick one of the hardest missions as goal (#3180)
This allows End Game as the goal even if long campaigns are present
2024-05-02 12:20:57 +02:00
Natalie Weizenbaum
9d478ba2bc Rules: Verify the default values of Options. (#2403)
* Verify the default values of `Option`s.

Since `Option.verify()` can handle normalization of option names, this allows options  to define defaults which rely on that normalization. For example, it allows a world to exclude certain locations by default.

This also makes it easier to catch errors if a world author accidentally sets an invalid default.

* Update Generate.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-02 12:19:15 +02:00
ken
3cc434cd78 Core: organize files on ingest via alpha, not ascii (#3029)
* organize files on ingest via alpha, not ascii

* Change from lower() to casefold()
2024-05-02 12:14:50 +02:00
Scipio Wright
31a5696526 Noita: Add more location groups, capitalize existing ones (#3141)
* Add location groups for each region

* Capitalize existing location groups

* Capitalize new boss location group names

* Update comment with capitalization

* Capitalize location_type in reigons.py
2024-05-02 12:02:14 +02:00
palex00
7bdf9a643c Updating Poptracker-Pack-Link for Pokémon Emerald as the old one was no longer maintained and did not work with 0.4.5 (#3193)
* Replaced the outdated Tracker Pack with a new one that is also pinned in the Discord channel

* Same change but for Spanish

* Update setup_en.md

* catching the bottom link as well

* See English Setupguide
2024-05-02 11:56:35 +02:00
Scipio Wright
c64c80aac0 TUNIC: Location groups for each area of the game (#3024)
* huzzah, location groups

* scope creep pog

* Apply suggestion to the other spot it is applicable at too

* apply berserker's suggestion

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Remove extra location group for shops

* Fire rod for magic wand

* Capitalize itme name groups

* Update docs to capitalize item name groups, remove the little section on aliases

since the aliases bit is really more for someone misremembering the name than anything else, like "fire rod" is because you played a lot of LttP, or Orb instead of Magic Orb is clear.

* Fix rule with item group name

* Capitalization is cool

* Fix merge mistake

* Add Flask group, remove Potions group

* Update docs to detail how to find item and location groups

* Revise per Vi's comment

* Fix test

* fuzzy matching please stop

* Remove test change that was meant for a different branch

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-05-02 10:02:59 +02:00
Aaron Wagener
07d9d6165e Tests: Clean up some of the fill test helpers a bit (#2935)
* Tests: Clean up some of the fill test helpers a bit

* fix some formatting

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-02 10:01:59 +02:00
Star Rauchenberger
fc571ba356 Lingo: Expand sphere 1 under restrictive conditions (#3190) 2024-05-02 09:52:16 +02:00
Emily
ea6235e0d9 Core: Improve join/leave messages, add "HintGame" tag (#2859)
* Add better "verbs" on joining msg, and improve leaving msgs

* Add 'HintGame' tag, for projects like BKSudoku/APSudoku/HintMachine

* data in one place instead of 3

* Clean up 'ignore_game' loop to use any() instead

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
2024-05-02 09:38:49 +02:00
Aaron Wagener
6f8b8fc9c9 Options: Add an OptionError to specify bad options caused the failure (#2343)
* Options: Add an OptionError to specify bad options caused the failure

* inherit from ValueError instead of RuntimeError since this error should be thrown via bad input

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-02 09:22:50 +02:00
black-sliver
0ed0de3daa Setup: update cx_freeze to 7.x (#3195) 2024-05-01 01:48:32 +02:00
Star Rauchenberger
487a067d10 Lingo: Fix world load on frozen 3.8 (#3220)
* Lingo: Fix world load on frozen 3.8

* Fixed absolute imports in unit test

* Made unpickling safer
2024-04-29 20:38:29 +02:00
Doug Hoskisson
fc4e6adff5 Core: some CommonContext typing (#3227) 2024-04-29 07:26:30 +02:00
t3hf1gm3nt
9cdc90513b [TLOZ]: Dark Rooms and Level 8 Logic Fixes (#3222)
Properly name the Book to Book of Magic in Rules.py so you can actually possibly be expected to use Magical Rod plus Book of Magic to get through dark rooms. No wonder we tend to see candles so early oops.

Also adding a rule that you need candles for access to Level 8 so you aren't required to time a Rod+Book shot against a moblin to burn the bush. Might make this a logic trick or something later.
2024-04-28 01:49:59 +02:00
Alchav
9afe45166c ALTTP: 0.4.6 fixes (#3215)
* Fix randomizer room logic

* Fix Triforce Hunt HUD always present

* Fix Circle of Pots enemy byte

* treasure_hunt_total for Murahdala text
2024-04-28 01:48:59 +02:00
LiquidCat64
9e20fa48e1 CV64: fix import that shouldn't be relative (#3223) 2024-04-28 01:41:30 +02:00
Zach Parks
e76ba928a8 WebHost: Prevent committing data packages with invalid checksums to database and prevent 500 error from invalid zip files. (#3206) 2024-04-26 22:18:12 -04:00
Aaron Wagener
4f1e696243 The Messenger: fix import that shouldn't be relative (#3219) 2024-04-26 21:29:01 +02:00
Fabian Dill
4756c76541 WebHost: remove JSON_AS_ASCII (#3209) 2024-04-24 06:36:35 +02:00
Fabian Dill
2f78860d8c Core/SNIClient/LttP/Factorio: switch to get_settings (#3208) 2024-04-24 06:24:44 +02:00
Scipio Wright
cca9778871 Delete worlds/sc2wol directory (#3202) 2024-04-23 12:58:38 -05:00
Fabian Dill
bb16fe284a Core: make open_filename log that it's asking (#3199) 2024-04-23 19:05:03 +02:00
97 changed files with 11927 additions and 1093 deletions

View File

@@ -11,8 +11,8 @@ from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar
import NetUtils
import Options
@@ -707,6 +707,14 @@ class CollectionState():
"""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)
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())
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())
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
@@ -714,8 +722,23 @@ class CollectionState():
Utils.deprecate("Use count instead.")
return self.count(item, player)
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name]
if found >= count:
return True
return False
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)
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
@@ -725,11 +748,12 @@ class CollectionState():
return False
def count_group(self, item_name_group: str, player: int) -> int:
found: int = 0
"""Returns the cumulative count of items from an item group present in state."""
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
found += player_prog_items[item_name]
return found
return sum(
player_prog_items[item_name]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)
# Item related
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:

View File

@@ -207,6 +207,8 @@ class CommonContext:
finished_game: bool
ready: bool
team: typing.Optional[int]
slot: typing.Optional[int]
auth: typing.Optional[str]
seed_name: typing.Optional[str]

22
Fill.py
View File

@@ -19,11 +19,12 @@ def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events()
new_state.sweep_for_events(locations=locations)
return new_state
@@ -66,7 +67,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
item_pool.pop(p)
break
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
if single_player_placement else None)
has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)
@@ -112,7 +114,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool)
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
@@ -170,7 +174,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
if cleanup_required:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
state = sweep_from_pool(
base_state, [], multiworld.get_filled_locations(item.player)
if single_player_placement else None)
for placement in placements:
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
placement.item.location = None
@@ -456,14 +462,16 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
if progitempool:
# "advancement/progression fill"
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
name="Progression")
if progitempool:
raise FillError(
f"Not enough locations for progression items. "

View File

@@ -120,7 +120,7 @@ def main(args=None, callback=ERmain):
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
for filename, yaml_data in weights_cache.items():
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
@@ -353,7 +353,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
raise Exception(f"Error generating meta option {option_key} for {game}.")
raise Options.OptionError(f"Error generating meta option {option_key} for {game}.")
def roll_linked_options(weights: dict) -> dict:
@@ -409,19 +409,19 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
try:
if option_key in game_weights:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
player_option = option.from_any(option.default) # call the from_any here to support default "random"
setattr(ret, option_key, player_option)
except Exception as e:
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
else:
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):

View File

@@ -688,7 +688,7 @@ class Context:
clients = 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)]
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))
@@ -803,14 +803,25 @@ async def on_client_disconnected(ctx: Context, client: Client):
await on_client_left(ctx, client)
_non_game_messages = {"HintGame": "hinting", "Tracker": "tracking", "TextOnly": "viewing"}
""" { tag: ui_message } """
async def on_client_joined(ctx: Context, client: Client):
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version)
verb = "tracking" if "Tracker" in client.tags else "playing"
for tag, verb in _non_game_messages.items():
if tag in client.tags:
final_verb = verb
break
else:
final_verb = "playing"
ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"{verb} {ctx.games[client.slot]} has joined. "
f"{final_verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}.",
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
ctx.notify_client(client, "Now that you are connected, "
@@ -825,8 +836,19 @@ async def on_client_left(ctx: Context, client: Client):
if len(ctx.clients[client.team][client.slot]) < 1:
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
version_str = '.'.join(str(x) for x in client.version)
for tag, verb in _non_game_messages.items():
if tag in client.tags:
final_verb = f"stopped {verb}"
break
else:
final_verb = "left"
ctx.broadcast_text_all(
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has {final_verb} the game. "
f"Client({version_str}), {client.tags}.",
{"type": "Part", "team": client.team, "slot": client.slot})
@@ -1507,15 +1529,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
self.ctx.notify_hints(self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
old_hints = list(set(hints) - new_hints)
if old_hints and not new_hints:
self.ctx.notify_hints(self.client.team, old_hints)
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]
if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
@@ -1527,7 +1547,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
hints = found_hints + old_hints
while can_pay > 0:
if not not_found_hints:
break
@@ -1537,6 +1557,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
self.ctx.notify_hints(self.client.team, hints)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
@@ -1550,7 +1571,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
self.ctx.notify_hints(self.client.team, hints)
self.ctx.save()
return True
@@ -1631,7 +1651,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
team, slot = ctx.connect_names[args['name']]
game = ctx.games[slot]
ignore_game = ("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game")
ignore_game = not args.get("game") and any(tag in _non_game_messages for tag in args["tags"])
if not ignore_game and args['game'] != game:
errors.add('InvalidGame')
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
@@ -2198,7 +2220,7 @@ async def console(ctx: Context):
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
defaults = Utils.get_options()["server_options"].as_dict()
defaults = Utils.get_settings()["server_options"].as_dict()
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int)

View File

@@ -21,6 +21,10 @@ if typing.TYPE_CHECKING:
import pathlib
class OptionError(ValueError):
pass
class Visibility(enum.IntFlag):
none = 0b0000
template = 0b0001

View File

@@ -65,6 +65,7 @@ Currently, the following games are supported:
* Castlevania 64
* A Short Hike
* Yoshi's Island
* Mario & Luigi: Superstar Saga
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -282,7 +282,7 @@ class SNESState(enum.IntEnum):
def launch_sni() -> None:
sni_path = Utils.get_options()["sni_options"]["sni_path"]
sni_path = Utils.get_settings()["sni_options"]["sni_path"]
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
@@ -565,7 +565,7 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try:
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
@@ -654,7 +654,7 @@ async def game_watcher(ctx: SNIContext) -> None:
async def run_game(romfile: str) -> None:
auto_start = typing.cast(typing.Union[bool, str],
Utils.get_options()["sni_options"].get("snes_rom_start", True))
Utils.get_settings()["sni_options"].get("snes_rom_start", True))
if auto_start is True:
import webbrowser
webbrowser.open(romfile)

View File

@@ -201,7 +201,7 @@ def cache_path(*path: str) -> str:
def output_path(*path: str) -> str:
if hasattr(output_path, 'cached_path'):
return os.path.join(output_path.cached_path, *path)
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
output_path.cached_path = user_path(get_settings()["general_options"]["output_path"])
path = os.path.join(output_path.cached_path, *path)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
@@ -619,6 +619,8 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \
-> typing.Optional[str]:
logging.info(f"Opening file input dialog for {title}.")
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None

View File

@@ -150,6 +150,7 @@ def host_room(room: UUID):
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port

View File

@@ -49,7 +49,7 @@
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
<a href="{{ url_for("tutorial_landing", _anchor = game_name | urlencode) }}">Setup Guides</a>
{% endif %}
{% if world.web.options_page is string %}
<span class="link-spacer">|</span>

View File

@@ -63,12 +63,13 @@ def process_multidata(compressed_multidata, files={}):
game_data = games_package_schema.validate(game_data)
game_data = {key: value for key, value in sorted(game_data.items())}
game_data["checksum"] = data_package_checksum(game_data)
game_data_package = GameDataPackage(checksum=game_data["checksum"],
data=pickle.dumps(game_data))
if original_checksum != game_data["checksum"]:
raise Exception(f"Original checksum {original_checksum} != "
f"calculated checksum {game_data['checksum']} "
f"for game {game}.")
game_data_package = GameDataPackage(checksum=game_data["checksum"],
data=pickle.dumps(game_data))
decompressed_multidata["datapackage"][game] = {
"version": game_data.get("version", 0),
"checksum": game_data["checksum"],
@@ -192,6 +193,8 @@ def uploads():
res = upload_zip_to_db(zfile)
except VersionException:
flash(f"Could not load multidata. Wrong Version detected.")
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
else:
if res is str:
return res

View File

@@ -110,6 +110,11 @@ local IsItemable = function()
end
local is_game_complete = function()
-- If the Cannary Byte is 0xFF, then the save RAM is untrustworthy
if memory.read_u8(canary_byte) == 0xFF then
return game_complete
end
-- If on the title screen don't read RAM, RAM can't be trusted yet
if IsOnTitle() then return game_complete end

View File

@@ -92,6 +92,9 @@
/worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u
# Mario & Luigi: Superstar Saga
/worlds/mlss/ @jamesbrq
# Meritous
/worlds/meritous/ @FelicitusNeko

View File

@@ -45,9 +45,6 @@
# TODO
#CACHE_TYPE: "simple"
# TODO
#JSON_AS_ASCII: false
# Host Address. This is the address encoded into the patch that will be used for client auto-connect.
#HOST_ADDRESS: archipelago.gg

View File

@@ -21,7 +21,7 @@ from pathlib import Path
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
try:
requirement = 'cx-Freeze>=6.15.16,<7'
requirement = 'cx-Freeze>=7.0.0'
import pkg_resources
try:
pkg_resources.require(requirement)
@@ -228,8 +228,8 @@ class BuildCommand(setuptools.command.build.build):
# Override cx_Freeze's build_exe command for pre and post build steps
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
user_options = cx_Freeze.command.build_exe.build_exe.user_options + [
('yes', 'y', 'Answer "yes" to all questions.'),
('extra-data=', None, 'Additional files to add.'),
]

View File

@@ -1,7 +1,7 @@
from argparse import Namespace
from typing import List, Optional, Tuple, Type, Union
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
@@ -17,19 +17,21 @@ def setup_solo_multiworld(
:param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
steps through pre_fill
:param seed: The seed to be used when creating this multiworld
:return: The generated multiworld
"""
return setup_multiworld(world_type, steps, seed)
def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps,
seed: Optional[int] = None) -> MultiWorld:
seed: Optional[int] = None) -> MultiWorld:
"""
Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and
calling the provided gen steps.
:param worlds: type/s of worlds to generate a multiworld for
:param steps: gen steps that should be called before returning. Default calls through pre_fill
:param worlds: Type/s of worlds to generate a multiworld for
:param steps: Gen steps that should be called before returning. Default calls through pre_fill
:param seed: The seed to be used when creating this multiworld
:return: The generated multiworld
"""
if not isinstance(worlds, list):
worlds = [worlds]
@@ -49,3 +51,59 @@ def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple
for step in steps:
call_all(multiworld, step)
return multiworld
class TestWorld(World):
game = f"Test Game"
item_name_to_id = {}
location_name_to_id = {}
hidden = True
def generate_test_multiworld(players: int = 1) -> MultiWorld:
"""
Generates a multiworld using a special Test Case World class, and seed of 0.
:param players: Number of players to generate the multiworld for
:return: The generated test multiworld
"""
multiworld = setup_multiworld([TestWorld] * players, seed=0)
multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)]
return multiworld
def generate_locations(count: int, player_id: int, region: Region, address: Optional[int] = None,
tag: str = "") -> List[Location]:
"""
Generates the specified amount of locations for the player and adds them to the specified region.
:param count: Number of locations to create
:param player_id: ID of the player to create the locations for
:param address: Address for the specified locations. They will all share the same address if multiple are created
:param region: Parent region to add these locations to
:param tag: Tag to add to the name of the generated locations
:return: List containing the created locations
"""
prefix = f"player{player_id}{tag}_location"
locations = [Location(player_id, f"{prefix}{i}", address, region) for i in range(count)]
region.locations += locations
return locations
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
"""
Generates the specified amount of items for the target player.
:param count: The amount of items to create
:param player_id: ID of the player to create the items for
:param advancement: Whether the created items should be advancement
:param code: The code the items should be created with
:return: List containing the created items
"""
item_type = "prog" if advancement else ""
classification = ItemClassification.progression if advancement else ItemClassification.filler
items = [Item(f"player{player_id}_{item_type}item{i}", classification, code, player_id) for i in range(count)]
return items

View File

@@ -1,41 +1,15 @@
from typing import List, Iterable
import unittest
import Options
from Options import Accessibility
from worlds.AutoWorld import World
from test.general import generate_items, generate_locations, generate_test_multiworld
from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
distribute_early_items, distribute_items_restrictive
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \
ItemClassification, CollectionState
ItemClassification
from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
def generate_multiworld(players: int = 1) -> MultiWorld:
multiworld = MultiWorld(players)
multiworld.set_seed(0)
multiworld.player_name = {}
multiworld.state = CollectionState(multiworld)
for i in range(players):
player_id = i+1
world = World(multiworld, player_id)
multiworld.game[player_id] = f"Game {player_id}"
multiworld.worlds[player_id] = world
multiworld.player_name[player_id] = "Test Player " + str(player_id)
region = Region("Menu", player_id, multiworld, "Menu Region Hint")
multiworld.regions.append(region)
for option_key, option in Options.PerGameCommonOptions.type_hints.items():
if hasattr(multiworld, option_key):
getattr(multiworld, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
else:
setattr(multiworld, option_key, {player_id: option.from_any(getattr(option, "default"))})
# TODO - remove this loop once all worlds use options dataclasses
world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id]
for option_key in world.options_dataclass.type_hints})
return multiworld
class PlayerDefinition(object):
multiworld: MultiWorld
id: int
@@ -55,12 +29,12 @@ class PlayerDefinition(object):
self.regions = [menu]
def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
region_tag = "_region" + str(len(self.regions))
region_name = "player" + str(self.id) + region_tag
region = Region("player" + str(self.id) + region_tag, self.id, self.multiworld)
self.locations += generate_locations(size, self.id, None, region, region_tag)
region_tag = f"_region{len(self.regions)}"
region_name = f"player{self.id}{region_tag}"
region = Region(f"player{self.id}{region_tag}", self.id, self.multiworld)
self.locations += generate_locations(size, self.id, region, None, region_tag)
entrance = Entrance(self.id, region_name + "_entrance", parent)
entrance = Entrance(self.id, f"{region_name}_entrance", parent)
parent.exits.append(entrance)
entrance.connect(region)
entrance.access_rule = access_rule
@@ -94,7 +68,7 @@ def region_contains(region: Region, item: Item) -> bool:
def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
menu = multiworld.get_region("Menu", player_id)
locations = generate_locations(location_count, player_id, None, menu)
locations = generate_locations(location_count, player_id, menu, None)
prog_items = generate_items(prog_item_count, player_id, True)
multiworld.itempool += prog_items
basic_items = generate_items(basic_item_count, player_id, False)
@@ -103,28 +77,6 @@ def generate_player_data(multiworld: MultiWorld, player_id: int, location_count:
return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items)
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
locations = []
prefix = "player" + str(player_id) + tag + "_location"
for i in range(count):
name = prefix + str(i)
location = Location(player_id, name, address, region)
locations.append(location)
region.locations.append(location)
return locations
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
items = []
item_type = "prog" if advancement else ""
for i in range(count):
name = "player" + str(player_id) + "_" + item_type + "item" + str(i)
items.append(Item(name,
ItemClassification.progression if advancement else ItemClassification.filler,
code, player_id))
return items
def names(objs: list) -> Iterable[str]:
return map(lambda o: o.name, objs)
@@ -132,7 +84,7 @@ def names(objs: list) -> Iterable[str]:
class TestFillRestrictive(unittest.TestCase):
def test_basic_fill(self):
"""Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
item0 = player1.prog_items[0]
@@ -150,7 +102,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_ordered_fill(self):
"""Tests `fill_restrictive` fulfills set rules"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
items = player1.prog_items
locations = player1.locations
@@ -167,7 +119,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_partial_fill(self):
"""Tests that `fill_restrictive` returns unfilled locations"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 3, 2)
item0 = player1.prog_items[0]
@@ -193,7 +145,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_minimal_fill(self):
"""Test that fill for minimal player can have unreachable items"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
items = player1.prog_items
@@ -218,7 +170,7 @@ class TestFillRestrictive(unittest.TestCase):
the non-minimal player get all items.
"""
multiworld = generate_multiworld(2)
multiworld = generate_test_multiworld(2)
player1 = generate_player_data(multiworld, 1, 3, 3)
player2 = generate_player_data(multiworld, 2, 3, 3)
@@ -245,11 +197,11 @@ class TestFillRestrictive(unittest.TestCase):
# all of player2's locations and items should be accessible (not all of player1's)
for item in player2.prog_items:
self.assertTrue(multiworld.state.has(item.name, player2.id),
f'{item} is unreachable in {item.location}')
f"{item} is unreachable in {item.location}")
def test_reversed_fill(self):
"""Test a different set of rules can be satisfied"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
item0 = player1.prog_items[0]
@@ -268,7 +220,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_multi_step_fill(self):
"""Test that fill is able to satisfy multiple spheres"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 4, 4)
items = player1.prog_items
@@ -293,7 +245,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_impossible_fill(self):
"""Test that fill raises an error when it can't place any items"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
items = player1.prog_items
locations = player1.locations
@@ -310,7 +262,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_circular_fill(self):
"""Test that fill raises an error when it can't place all items"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 3, 3)
item0 = player1.prog_items[0]
@@ -331,7 +283,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_competing_fill(self):
"""Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
item0 = player1.prog_items[0]
@@ -348,7 +300,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_multiplayer_fill(self):
"""Test that items can be placed across worlds"""
multiworld = generate_multiworld(2)
multiworld = generate_test_multiworld(2)
player1 = generate_player_data(multiworld, 1, 2, 2)
player2 = generate_player_data(multiworld, 2, 2, 2)
@@ -369,7 +321,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_multiplayer_rules_fill(self):
"""Test that fill across worlds satisfies the rules"""
multiworld = generate_multiworld(2)
multiworld = generate_test_multiworld(2)
player1 = generate_player_data(multiworld, 1, 2, 2)
player2 = generate_player_data(multiworld, 2, 2, 2)
@@ -393,7 +345,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_restrictive_progress(self):
"""Test that various spheres with different requirements can be filled"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, prog_item_count=25)
items = player1.prog_items.copy()
multiworld.completion_condition[player1.id] = lambda state: state.has_all(
@@ -417,7 +369,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_swap_to_earlier_location_with_item_rule(self):
"""Test that item swap happens and works as intended"""
# test for PR#1109
multiworld = generate_multiworld(1)
multiworld = generate_test_multiworld(1)
player1 = generate_player_data(multiworld, 1, 4, 4)
locations = player1.locations[:] # copy required
items = player1.prog_items[:] # copy required
@@ -442,7 +394,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_swap_to_earlier_location_with_item_rule2(self):
"""Test that swap works before all items are placed"""
multiworld = generate_multiworld(1)
multiworld = generate_test_multiworld(1)
player1 = generate_player_data(multiworld, 1, 5, 5)
locations = player1.locations[:] # copy required
items = player1.prog_items[:] # copy required
@@ -484,7 +436,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_double_sweep(self):
"""Test that sweep doesn't duplicate Event items when sweeping"""
# test for PR1114
multiworld = generate_multiworld(1)
multiworld = generate_test_multiworld(1)
player1 = generate_player_data(multiworld, 1, 1, 1)
location = player1.locations[0]
location.address = None
@@ -498,7 +450,7 @@ class TestFillRestrictive(unittest.TestCase):
def test_correct_item_instance_removed_from_pool(self):
"""Test that a placed item gets removed from the submitted pool"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(multiworld, 1, 2, 2)
player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
@@ -515,7 +467,7 @@ class TestFillRestrictive(unittest.TestCase):
class TestDistributeItemsRestrictive(unittest.TestCase):
def test_basic_distribute(self):
"""Test that distribute_items_restrictive is deterministic"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -535,7 +487,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_excluded_distribute(self):
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -550,7 +502,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_non_excluded_item_distribute(self):
"""Test that useful items aren't placed on excluded locations"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -565,7 +517,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_too_many_excluded_distribute(self):
"""Test that fill fails if it can't place all progression items due to too many excluded locations"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -578,7 +530,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_non_excluded_item_must_distribute(self):
"""Test that fill fails if it can't place useful items due to too many excluded locations"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -593,7 +545,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_priority_distribute(self):
"""Test that priority locations receive advancement items"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -608,7 +560,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_excess_priority_distribute(self):
"""Test that if there's more priority locations than advancement items, they can still fill"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
@@ -623,7 +575,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_multiple_world_priority_distribute(self):
"""Test that priority fill can be satisfied for multiple worlds"""
multiworld = generate_multiworld(3)
multiworld = generate_test_multiworld(3)
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
player2 = generate_player_data(
@@ -653,7 +605,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_can_remove_locations_in_fill_hook(self):
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -673,12 +625,12 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_seed_robust_to_item_order(self):
"""Test deterministic fill"""
mw1 = generate_multiworld()
mw1 = generate_test_multiworld()
gen1 = generate_player_data(
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
distribute_items_restrictive(mw1)
mw2 = generate_multiworld()
mw2 = generate_test_multiworld()
gen2 = generate_player_data(
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
mw2.itempool.append(mw2.itempool.pop(0))
@@ -691,12 +643,12 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_seed_robust_to_location_order(self):
"""Test deterministic fill even if locations in a region are reordered"""
mw1 = generate_multiworld()
mw1 = generate_test_multiworld()
gen1 = generate_player_data(
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
distribute_items_restrictive(mw1)
mw2 = generate_multiworld()
mw2 = generate_test_multiworld()
gen2 = generate_player_data(
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
reg = mw2.get_region("Menu", gen2.id)
@@ -710,7 +662,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_can_reserve_advancement_items_for_general_fill(self):
"""Test that priority locations fill still satisfies item rules"""
multiworld = generate_multiworld()
multiworld = generate_test_multiworld()
player1 = generate_player_data(
multiworld, 1, location_count=5, prog_item_count=5)
items = player1.prog_items
@@ -727,7 +679,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_non_excluded_local_items(self):
"""Test that local items get placed locally in a multiworld"""
multiworld = generate_multiworld(2)
multiworld = generate_test_multiworld(2)
player1 = generate_player_data(
multiworld, 1, location_count=5, basic_item_count=5)
player2 = generate_player_data(
@@ -748,7 +700,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
def test_early_items(self) -> None:
"""Test that the early items API successfully places items early"""
mw = generate_multiworld(2)
mw = generate_test_multiworld(2)
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
mw.early_items[1][player1.basic_items[0].name] = 1
@@ -803,11 +755,11 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
if location.item and location.item == item:
return True
self.fail("Expected " + region.name + " to contain " + item.name +
"\n Contains" + str(list(map(lambda location: location.item, region.locations))))
self.fail(f"Expected {region.name} to contain {item.name}.\n"
f"Contains{list(map(lambda location: location.item, region.locations))}")
def setUp(self) -> None:
multiworld = generate_multiworld(2)
multiworld = generate_test_multiworld(2)
self.multiworld = multiworld
player1 = generate_player_data(
multiworld, 1, prog_item_count=2, basic_item_count=40)

View File

@@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if loc in all_state_base.events:
all_state_base.events.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
name="LttP Dungeon Items")

View File

@@ -276,13 +276,14 @@ def generate_itempool(world):
# set up item pool
additional_triforce_pieces = 0
treasure_hunt_total = 0
if multiworld.custom:
pool, placed_items, precollected_items, clock_mode, treasure_hunt_count = (
pool, placed_items, precollected_items, clock_mode, treasure_hunt_required = (
make_custom_item_pool(multiworld, player))
multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999)
else:
pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, additional_triforce_pieces = (
get_pool_core(multiworld, player))
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
additional_triforce_pieces) = get_pool_core(multiworld, player)
for item in precollected_items:
multiworld.push_precollected(item_factory(item, world))
@@ -337,7 +338,8 @@ def generate_itempool(world):
if clock_mode:
world.clock_mode = clock_mode
multiworld.worlds[player].treasure_hunt_count = treasure_hunt_count % 999
multiworld.worlds[player].treasure_hunt_required = treasure_hunt_required % 999
multiworld.worlds[player].treasure_hunt_total = treasure_hunt_total
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
@@ -590,7 +592,8 @@ def get_pool_core(world, player: int):
placed_items = {}
precollected_items = []
clock_mode: str = ""
treasure_hunt_count: int = 1
treasure_hunt_required: int = 0
treasure_hunt_total: int = 0
diff = difficulties[difficulty]
pool.extend(diff.alwaysitems)
@@ -679,20 +682,21 @@ def get_pool_core(world, player: int):
if 'triforce_hunt' in goal:
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
triforce_pieces = world.triforce_pieces_available[player].value + world.triforce_pieces_extra[player].value
treasure_hunt_total = (world.triforce_pieces_available[player].value
+ world.triforce_pieces_extra[player].value)
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
percentage = float(world.triforce_pieces_percentage[player].value) / 100
triforce_pieces = int(round(world.triforce_pieces_required[player].value * percentage, 0))
treasure_hunt_total = int(round(world.triforce_pieces_required[player].value * percentage, 0))
else: # available
triforce_pieces = world.triforce_pieces_available[player].value
treasure_hunt_total = world.triforce_pieces_available[player].value
triforce_pieces = min(90, max(triforce_pieces, world.triforce_pieces_required[player].value))
triforce_pieces = min(90, max(treasure_hunt_total, world.triforce_pieces_required[player].value))
pieces_in_core = min(extraitems, triforce_pieces)
additional_pieces_to_place = triforce_pieces - pieces_in_core
pool.extend(["Triforce Piece"] * pieces_in_core)
extraitems -= pieces_in_core
treasure_hunt_count = world.triforce_pieces_required[player].value
treasure_hunt_required = world.triforce_pieces_required[player].value
for extra in diff.extras:
if extraitems >= len(extra):
@@ -733,7 +737,7 @@ def get_pool_core(world, player: int):
place_item(key_location, "Small Key (Universal)")
pool = pool[:-3]
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count,
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
additional_pieces_to_place)
@@ -749,7 +753,8 @@ def make_custom_item_pool(world, player):
placed_items = {}
precollected_items = []
clock_mode: str = ""
treasure_hunt_count: int = 1
treasure_hunt_required: int = 0
treasure_hunt_total: int = 0
def place_item(loc, item):
assert loc not in placed_items, "cannot place item twice"
@@ -844,7 +849,7 @@ def make_custom_item_pool(world, player):
if "triforce" in world.goal[player]:
pool.extend(["Triforce Piece"] * world.triforce_pieces_available[player])
itemtotal += world.triforce_pieces_available[player]
treasure_hunt_count = world.triforce_pieces_required[player]
treasure_hunt_required = world.triforce_pieces_required[player]
if timer in ['display', 'timed', 'timed_countdown']:
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
@@ -889,4 +894,4 @@ def make_custom_item_pool(world, player):
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}")
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count)
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required)

View File

@@ -433,7 +433,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
if multiworld.key_drop_shuffle[player]:
key_drop_enemies = {
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
0x4E20A, 0x4E326, 0x4E4F7, 0x4E686, 0x4E70C, 0x4E7C8, 0x4E7FA
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
}
for enemy in key_drop_enemies:
if rom.read_byte(enemy) == 0x12:
@@ -1269,7 +1269,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
# set up goals for treasure hunt
rom.write_int16(0x180163, local_world.treasure_hunt_count)
rom.write_int16(0x180163, local_world.treasure_hunt_required)
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
@@ -1859,7 +1859,7 @@ def apply_oof_sfx(rom, oof: str):
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
#Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
# Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
@@ -2482,16 +2482,16 @@ def write_strings(rom, world, player):
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
else:
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
if w.treasure_hunt_count > 1:
if w.treasure_hunt_required > 1:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \
(w.treasure_hunt_count, world.triforce_pieces_available[player])
(w.treasure_hunt_required, w.treasure_hunt_total)
else:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(w.treasure_hunt_count, world.triforce_pieces_available[player])
(w.treasure_hunt_required, w.treasure_hunt_total)
elif world.goal[player] in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
@@ -2500,20 +2500,20 @@ def write_strings(rom, world, player):
tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)]
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
if w.treasure_hunt_count > 1:
if w.treasure_hunt_required > 1:
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
(w.treasure_hunt_count, world.triforce_pieces_available[player])
(w.treasure_hunt_required, w.treasure_hunt_total)
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
(w.treasure_hunt_count, world.triforce_pieces_available[player])
(w.treasure_hunt_required, w.treasure_hunt_total)
else:
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
(w.treasure_hunt_count, world.triforce_pieces_available[player])
(w.treasure_hunt_required, w.treasure_hunt_total)
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
(w.treasure_hunt_count, world.triforce_pieces_available[player])
(w.treasure_hunt_required, w.treasure_hunt_total)
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
@@ -3021,7 +3021,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
options = Utils.get_settings()
if not file_name:
file_name = options["lttp_options"]["rom_file"]
if not os.path.exists(file_name):

View File

@@ -64,20 +64,24 @@ def set_rules(world):
if world.glitches_required[player] == 'no_glitches':
no_glitches_rules(world, player)
forbid_bomb_jump_requirements(world, player)
elif world.glitches_required[player] == 'overworld_glitches':
# Initially setting no_glitches_rules to set the baseline rules for some
# entrances. The overworld_glitches_rules set is primarily additive.
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
forbid_bomb_jump_requirements(world, player)
elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
underworld_glitches_rules(world, player)
bomb_jump_requirements(world, player)
elif world.glitches_required[player] == 'minor_glitches':
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
forbid_bomb_jump_requirements(world, player)
else:
raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}')
@@ -290,6 +294,9 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
set_rule(multiworld.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
set_rule(multiworld.get_location('Hyrule Castle - Map Guard Key Drop', player),
lambda state: can_kill_most_things(state, player, 1))
set_rule(multiworld.get_entrance('Sewers Door', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal and multiworld.mode[
@@ -536,6 +543,8 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
if multiworld.pot_shuffle[player]:
set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
@@ -554,8 +563,8 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or
((item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
for location in randomizer_room_chests:
set_rule(multiworld.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
set_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))))
# Once again it is possible to need more than 7 keys...
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
@@ -900,7 +909,6 @@ def no_glitches_rules(world, player):
add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override
forbid_bomb_jump_requirements(world, player)
add_conditional_lamps(world, player)
def fake_flipper_rules(world, player):
@@ -928,12 +936,20 @@ def fake_flipper_rules(world, player):
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player))
def forbid_bomb_jump_requirements(world, player):
def bomb_jump_requirements(multiworld, player):
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
for location in DMs_room_chests:
add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Paradox Cave Bomb Jump', player), lambda state: False)
set_rule(world.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: False)
add_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player), combine="or")
set_rule(multiworld.get_entrance('Paradox Cave Bomb Jump', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: can_use_bombs(state, player))
def forbid_bomb_jump_requirements(multiworld, player):
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
for location in DMs_room_chests:
add_rule(multiworld.get_location(location, player), lambda state: state.has('Hookshot', player))
set_rule(multiworld.get_entrance('Paradox Cave Bomb Jump', player), lambda state: False)
set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: False)
DW_Entrances = ['Bumper Cave (Bottom)',
@@ -1012,9 +1028,6 @@ def add_conditional_lamps(world, player):
def open_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player),
lambda state: can_kill_most_things(state, player, 1))
def basement_key_rule(state):
if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player):
return state._lttp_has_key("Small Key (Hyrule Castle)", player, 2)
@@ -1023,7 +1036,7 @@ def open_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2))
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), basement_key_rule)
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1))
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1))
@@ -1031,8 +1044,10 @@ def open_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and
state.has('Big Key (Hyrule Castle)', player))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
and state.has('Big Key (Hyrule Castle)', player)
and (world.enemy_health[player] in ("easy", "default")
or can_kill_most_things(state, player, 1)))
def swordless_rules(world, player):
@@ -1062,6 +1077,7 @@ def add_connection(parent_name, target_name, entrance_name, world, player):
parent.exits.append(connection)
connection.connect(target)
def standard_rules(world, player):
add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
world.get_entrance('Uncle S&Q', player).hide_path = True
@@ -1073,18 +1089,23 @@ def standard_rules(world, player):
if world.small_key_shuffle[player] != small_key_shuffle.option_universal:
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
and can_kill_most_things(state, player, 2))
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
and can_kill_most_things(state, player, 1))
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and
state.has('Big Key (Hyrule Castle)', player))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
and state.has('Big Key (Hyrule Castle)', player)
and (world.enemy_health[player] in ("easy", "default")
or can_kill_most_things(state, player, 1)))
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)
and can_kill_most_things(state, player, 1))
else:
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state.has('Big Key (Hyrule Castle)', player))

View File

@@ -30,7 +30,7 @@ def can_shoot_arrows(state: CollectionState, player: int) -> bool:
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
count = state.multiworld.worlds[player].treasure_hunt_count
count = state.multiworld.worlds[player].treasure_hunt_required
return state.count('Triforce Piece', player) + state.count('Power Star', player) >= count

View File

@@ -261,7 +261,8 @@ class ALTTPWorld(World):
fix_fake_world: bool = True
clock_mode: str = ""
treasure_hunt_count: int = 1
treasure_hunt_required: int = 0
treasure_hunt_total: int = 0
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()

View File

@@ -15,7 +15,7 @@ class TestDungeon(LTTPTestBase):
self.remove_exits = [] # Block dungeon exits
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
create_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1)

View File

@@ -33,22 +33,26 @@ class TestGanonsTower(TestDungeon):
["Ganons Tower - Randomizer Room - Top Left", False, []],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hookshot']],
["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Top Right", False, []],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hookshot']],
["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Bottom Left", False, []],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hookshot']],
["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Bottom Right", False, []],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hookshot']],
["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Firesnake Room", False, []],
["Ganons Tower - Firesnake Room", False, [], ['Hammer']],

View File

@@ -16,7 +16,7 @@ class TestInverted(TestBase, LTTPTestBase):
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.mode[1].value = 2
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons()
create_shops(self.multiworld, 1)

View File

@@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase, LTTPTestBase):
self.multiworld.mode[1].value = 2
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons()

View File

@@ -17,7 +17,7 @@ class TestInvertedOWG(TestBase, LTTPTestBase):
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.mode[1].value = 2
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons()

View File

@@ -13,7 +13,7 @@ class TestMinor(TestBase, LTTPTestBase):
self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.world.er_seed = 0
self.world.create_regions()

View File

@@ -14,7 +14,7 @@ class TestVanillaOWG(TestBase, LTTPTestBase):
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items()

View File

@@ -13,7 +13,7 @@ class TestVanilla(TestBase, LTTPTestBase):
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("no_glitches")
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items()

View File

@@ -1,6 +1,38 @@
# Celeste 64 - Changelog
## v1.2
### Features:
- New optional Location Checks
- Friendsanity
- Signsanity
- Carsanity
- Move Shuffle
- Basic movement abilities can be shuffled into the item pool
- Ground Dash
- Air Dash
- Skid Jump
- Climb
- Logic Difficulty
- Completely overhauled logic system
- Standard or Hard logic difficulty can be chosen
- Badeline Chasers
- Opt-in options which cause Badelines to start following you as you play, which will kill on contact
- These can be set to spawn based on either:
- The number of locations you've checked
- The number of Strawberry items you've received
- How fast they follow behind you can be specified
### Quality of Life:
- The maximum number of Strawberries in the item pool can be directly set
- The required amount of Strawberries is now set via percentage
- All items beyond the amount placed in the item pool will be `Raspberry` items, which have no effect
- Any unique items placed into the `start_inventory` will not be placed into the item pool
## v1.1 - First Stable Release
### Features:

View File

@@ -16,43 +16,29 @@ class Celeste64ItemData(NamedTuple):
type: ItemClassification = ItemClassification.filler
item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.strawberry: Celeste64ItemData(
code = celeste_64_base_id + 0,
type=ItemClassification.progression_skip_balancing,
),
ItemName.dash_refill: Celeste64ItemData(
code = celeste_64_base_id + 1,
type=ItemClassification.progression,
),
ItemName.double_dash_refill: Celeste64ItemData(
code = celeste_64_base_id + 2,
type=ItemClassification.progression,
),
ItemName.feather: Celeste64ItemData(
code = celeste_64_base_id + 3,
type=ItemClassification.progression,
),
ItemName.coin: Celeste64ItemData(
code = celeste_64_base_id + 4,
type=ItemClassification.progression,
),
ItemName.cassette: Celeste64ItemData(
code = celeste_64_base_id + 5,
type=ItemClassification.progression,
),
ItemName.traffic_block: Celeste64ItemData(
code = celeste_64_base_id + 6,
type=ItemClassification.progression,
),
ItemName.spring: Celeste64ItemData(
code = celeste_64_base_id + 7,
type=ItemClassification.progression,
),
ItemName.breakables: Celeste64ItemData(
code = celeste_64_base_id + 8,
type=ItemClassification.progression,
)
collectable_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.strawberry: Celeste64ItemData(celeste_64_base_id + 0x0, ItemClassification.progression_skip_balancing),
ItemName.raspberry: Celeste64ItemData(celeste_64_base_id + 0x9, ItemClassification.filler),
}
unlockable_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.dash_refill: Celeste64ItemData(celeste_64_base_id + 0x1, ItemClassification.progression),
ItemName.double_dash_refill: Celeste64ItemData(celeste_64_base_id + 0x2, ItemClassification.progression),
ItemName.feather: Celeste64ItemData(celeste_64_base_id + 0x3, ItemClassification.progression),
ItemName.coin: Celeste64ItemData(celeste_64_base_id + 0x4, ItemClassification.progression),
ItemName.cassette: Celeste64ItemData(celeste_64_base_id + 0x5, ItemClassification.progression),
ItemName.traffic_block: Celeste64ItemData(celeste_64_base_id + 0x6, ItemClassification.progression),
ItemName.spring: Celeste64ItemData(celeste_64_base_id + 0x7, ItemClassification.progression),
ItemName.breakables: Celeste64ItemData(celeste_64_base_id + 0x8, ItemClassification.progression),
}
move_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.ground_dash: Celeste64ItemData(celeste_64_base_id + 0xA, ItemClassification.progression),
ItemName.air_dash: Celeste64ItemData(celeste_64_base_id + 0xB, ItemClassification.progression),
ItemName.skid_jump: Celeste64ItemData(celeste_64_base_id + 0xC, ItemClassification.progression),
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
}
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table, **unlockable_item_data_table, **move_item_data_table}
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}

View File

@@ -16,127 +16,67 @@ class Celeste64LocationData(NamedTuple):
address: Optional[int] = None
location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.strawberry_1 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 0,
),
LocationName.strawberry_2 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 1,
),
LocationName.strawberry_3 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 2,
),
LocationName.strawberry_4 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 3,
),
LocationName.strawberry_5 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 4,
),
LocationName.strawberry_6 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 5,
),
LocationName.strawberry_7 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 6,
),
LocationName.strawberry_8 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 7,
),
LocationName.strawberry_9 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 8,
),
LocationName.strawberry_10 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 9,
),
LocationName.strawberry_11 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 10,
),
LocationName.strawberry_12 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 11,
),
LocationName.strawberry_13 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 12,
),
LocationName.strawberry_14 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 13,
),
LocationName.strawberry_15 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 14,
),
LocationName.strawberry_16 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 15,
),
LocationName.strawberry_17 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 16,
),
LocationName.strawberry_18 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 17,
),
LocationName.strawberry_19 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 18,
),
LocationName.strawberry_20 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 19,
),
LocationName.strawberry_21 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 20,
),
LocationName.strawberry_22 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 21,
),
LocationName.strawberry_23 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 22,
),
LocationName.strawberry_24 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 23,
),
LocationName.strawberry_25 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 24,
),
LocationName.strawberry_26 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 25,
),
LocationName.strawberry_27 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 26,
),
LocationName.strawberry_28 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 27,
),
LocationName.strawberry_29 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 28,
),
LocationName.strawberry_30 : Celeste64LocationData(
region = "Forsaken City",
address = celeste_64_base_id + 29,
)
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
}
friend_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
}
sign_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
}
car_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
}
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
**friend_location_data_table,
**sign_location_data_table,
**car_location_data_table}
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}

View File

@@ -1,4 +1,5 @@
strawberry = "Strawberry"
raspberry = "Raspberry"
dash_refill = "Dash Refills"
double_dash_refill = "Double Dash Refills"
@@ -9,3 +10,8 @@ cassette = "Cassettes"
traffic_block = "Traffic Blocks"
spring = "Springs"
breakables = "Breakable Blocks"
ground_dash = "Ground Dash"
air_dash = "Air Dash"
skid_jump = "Skid Jump"
climb = "Climb"

View File

@@ -29,3 +29,25 @@ strawberry_27 = "Distant Feather Cassette Strawberry"
strawberry_28 = "Feather Arches Cassette Strawberry"
strawberry_29 = "North-East Tower Cassette Strawberry"
strawberry_30 = "Badeline Cassette Strawberry"
# Friend Locations
granny_1 = "Granny Conversation 1"
granny_2 = "Granny Conversation 2"
granny_3 = "Granny Conversation 3"
theo_1 = "Theo Conversation 1"
theo_2 = "Theo Conversation 2"
theo_3 = "Theo Conversation 3"
badeline_1 = "Badeline Conversation 1"
badeline_2 = "Badeline Conversation 2"
badeline_3 = "Badeline Conversation 3"
# Sign Locations
sign_1 = "Camera Sign"
sign_2 = "Skid Jump Sign"
sign_3 = "Dash Jump Sign"
sign_4 = "Lonely Sign"
sign_5 = "Credits Sign"
# Car Locations
car_1 = "Intro Car"
car_2 = "Secret Car"

View File

@@ -1,25 +1,124 @@
from dataclasses import dataclass
from Options import Range, DeathLink, PerGameCommonOptions
from Options import Choice, Range, Toggle, DeathLink, PerGameCommonOptions
class StrawberriesRequired(Range):
"""How many Strawberries you must receive to finish"""
display_name = "Strawberries Required"
range_start = 0
range_end = 20
default = 15
class DeathLinkAmnesty(Range):
"""How many deaths it takes to send a DeathLink"""
"""
How many deaths it takes to send a DeathLink
"""
display_name = "Death Link Amnesty"
range_start = 1
range_end = 30
default = 10
class TotalStrawberries(Range):
"""
How many Strawberries exist
"""
display_name = "Total Strawberries"
range_start = 0
range_end = 46
default = 20
class StrawberriesRequiredPercentage(Range):
"""
Percentage of existing Strawberries you must receive to finish
"""
display_name = "Strawberries Required Percentage"
range_start = 0
range_end = 100
default = 80
class LogicDifficulty(Choice):
"""
Whether the logic expects you to play the intended way, or to be able to use advanced tricks and skips
"""
display_name = "Logic Difficulty"
option_standard = 0
option_hard = 1
default = 0
class MoveShuffle(Toggle):
"""
Whether the following base movement abilities are shuffled into the item pool:
- Ground Dash
- Air Dash
- Skid Jump
- Climb
NOTE: Having Move Shuffle and Standard Logic Difficulty will guarantee that one of the four Move items will be immediately accessible
WARNING: Combining Move Shuffle and Hard Logic Difficulty can require very difficult tricks
"""
display_name = "Move Shuffle"
class Friendsanity(Toggle):
"""
Whether chatting with your friends grants location checks
"""
display_name = "Friendsanity"
class Signsanity(Toggle):
"""
Whether reading signs grants location checks
"""
display_name = "Signsanity"
class Carsanity(Toggle):
"""
Whether riding on cars grants location checks
"""
display_name = "Carsanity"
class BadelineChaserSource(Choice):
"""
What type of action causes more Badeline Chasers to start spawning
Locations: The number of locations you've checked contributes to Badeline Chasers
Strawberries: The number of Strawberry items you've received contributes to Badeline Chasers
"""
display_name = "Badeline Chaser Source"
option_locations = 0
option_strawberries = 1
default = 0
class BadelineChaserFrequency(Range):
"""
How many of the `Badeline Chaser Source` actions must occur to make each Badeline Chaser start spawning
NOTE: Choosing `0` disables Badeline Chasers entirely
WARNING: Turning on Badeline Chasers alongside Move Shuffle could result in extremely difficult situations
"""
display_name = "Badeline Chaser Frequency"
range_start = 0
range_end = 10
default = 0
class BadelineChaserSpeed(Range):
"""
How many seconds behind you each Badeline Chaser will be
"""
display_name = "Badeline Chaser Speed"
range_start = 2
range_end = 10
default = 3
@dataclass
class Celeste64Options(PerGameCommonOptions):
death_link: DeathLink
death_link_amnesty: DeathLinkAmnesty
strawberries_required: StrawberriesRequired
total_strawberries: TotalStrawberries
strawberries_required_percentage: StrawberriesRequiredPercentage
logic_difficulty: LogicDifficulty
move_shuffle: MoveShuffle
friendsanity: Friendsanity
signsanity: Signsanity
carsanity: Carsanity
badeline_chaser_source: BadelineChaserSource
badeline_chaser_frequency: BadelineChaserFrequency
badeline_chaser_speed: BadelineChaserSpeed

View File

@@ -1,3 +1,6 @@
from typing import Dict, List
from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule
from . import Celeste64World
@@ -5,100 +8,336 @@ from .Names import ItemName, LocationName
def set_rules(world: Celeste64World):
set_rule(world.multiworld.get_location(LocationName.strawberry_4, world.player),
lambda state: state.has_all({ItemName.traffic_block,
ItemName.breakables}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_5, world.player),
lambda state: state.has(ItemName.breakables, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_6, world.player),
lambda state: state.has(ItemName.dash_refill, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_8, world.player),
lambda state: state.has(ItemName.traffic_block, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_9, world.player),
lambda state: state.has(ItemName.dash_refill, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_11, world.player),
lambda state: state.has(ItemName.dash_refill, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_12, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.double_dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_13, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.breakables}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_14, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.feather}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_15, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.feather}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_16, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.feather}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_17, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.double_dash_refill,
ItemName.feather,
ItemName.traffic_block}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_18, world.player),
lambda state: state.has(ItemName.double_dash_refill, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_19, world.player),
lambda state: state.has_all({ItemName.double_dash_refill,
ItemName.spring}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_20, world.player),
lambda state: state.has_all({ItemName.dash_refill,
ItemName.feather,
ItemName.breakables}, world.player))
if world.options.logic_difficulty == "standard":
if world.options.move_shuffle:
world.active_logic_mapping = location_standard_moves_logic
else:
world.active_logic_mapping = location_standard_logic
else:
if world.options.move_shuffle:
world.active_logic_mapping = location_hard_moves_logic
else:
world.active_logic_mapping = location_hard_logic
set_rule(world.multiworld.get_location(LocationName.strawberry_21, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.traffic_block,
ItemName.breakables}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_22, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.dash_refill,
ItemName.breakables}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_23, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.dash_refill,
ItemName.coin}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_24, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.traffic_block,
ItemName.dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_25, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.dash_refill,
ItemName.double_dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_26, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_27, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.feather,
ItemName.coin,
ItemName.dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_28, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.feather,
ItemName.coin,
ItemName.dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_29, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.feather,
ItemName.coin,
ItemName.dash_refill}, world.player))
set_rule(world.multiworld.get_location(LocationName.strawberry_30, world.player),
lambda state: state.has_all({ItemName.cassette,
ItemName.feather,
ItemName.traffic_block,
ItemName.spring,
ItemName.breakables,
ItemName.dash_refill,
ItemName.double_dash_refill}, world.player))
for location in world.multiworld.get_locations(world.player):
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
if world.options.logic_difficulty == "standard":
if world.options.move_shuffle:
world.goal_logic_mapping = goal_standard_moves_logic
else:
world.goal_logic_mapping = goal_standard_logic
else:
if world.options.move_shuffle:
world.goal_logic_mapping = goal_hard_moves_logic
else:
world.goal_logic_mapping = goal_hard_logic
# Completion condition.
world.multiworld.completion_condition[world.player] = lambda state: (state.has(ItemName.strawberry,world.player,world.options.strawberries_required.value) and
state.has_all({ItemName.feather,
ItemName.traffic_block,
ItemName.breakables,
ItemName.dash_refill,
ItemName.double_dash_refill}, world.player))
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
goal_standard_logic: List[List[str]] = [[ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.double_dash_refill]]
goal_hard_logic: List[List[str]] = [[]]
goal_standard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]]
goal_hard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]]
location_standard_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_6: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_7: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_8: [[ItemName.traffic_block]],
LocationName.strawberry_9: [[ItemName.dash_refill]],
LocationName.strawberry_11: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.traffic_block, ItemName.double_dash_refill]],
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables],
[ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring]],
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables],
[ItemName.traffic_block, ItemName.feather, ItemName.breakables]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill],
[ItemName.cassette, ItemName.traffic_block]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables]],
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.sign_2: [[ItemName.breakables]],
LocationName.sign_3: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.car_2: [[ItemName.breakables]],
}
location_hard_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_13: [[ItemName.breakables]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
LocationName.strawberry_20: [[ItemName.breakables]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_22: [[ItemName.cassette]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin]],
LocationName.strawberry_24: [[ItemName.cassette]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill]],
LocationName.strawberry_26: [[ItemName.cassette]],
LocationName.strawberry_27: [[ItemName.cassette]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather]],
LocationName.strawberry_29: [[ItemName.cassette]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables]],
LocationName.sign_2: [[ItemName.breakables]],
LocationName.car_2: [[ItemName.breakables]],
}
location_standard_moves_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.strawberry_2: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.strawberry_3: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_5: [[ItemName.air_dash]],
LocationName.strawberry_6: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_7: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_8: [[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_9: [[ItemName.dash_refill, ItemName.air_dash]],
LocationName.strawberry_10: [[ItemName.climb]],
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.ground_dash],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.skid_jump],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring, ItemName.air_dash]],
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.granny_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.granny_2: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.granny_3: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.sign_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.sign_3: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
}
location_hard_moves_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_3: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_5: [[ItemName.ground_dash],
[ItemName.air_dash]],
LocationName.strawberry_8: [[ItemName.traffic_block],
[ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_10: [[ItemName.air_dash],
[ItemName.climb]],
LocationName.strawberry_11: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_12: [[ItemName.feather],
[ItemName.ground_dash],
[ItemName.air_dash]],
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash],
[ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_15: [[ItemName.feather],
[ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
LocationName.strawberry_18: [[ItemName.air_dash, ItemName.climb],
[ItemName.double_dash_refill, ItemName.air_dash]],
LocationName.strawberry_19: [[ItemName.air_dash, ItemName.skid_jump],
[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
[ItemName.spring, ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_20: [[ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash],
[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash],
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.ground_dash],
[ItemName.cassette, ItemName.air_dash]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.air_dash, ItemName.skid_jump],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash],
[ItemName.cassette, ItemName.coin, ItemName.ground_dash],
[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.air_dash],
[ItemName.cassette, ItemName.feather, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.feather, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.ground_dash, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.feather, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
}
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
if loc not in world.active_logic_mapping:
return True
for possible_access in world.active_logic_mapping[loc]:
if state.has_all(possible_access, world.player):
return True
return False
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
return False
for possible_access in world.goal_logic_mapping:
if state.has_all(possible_access, world.player):
return True
return False

View File

@@ -1,10 +1,12 @@
from typing import List
from copy import deepcopy
from typing import Dict, List
from BaseClasses import ItemClassification, Region, Tutorial
from BaseClasses import ItemClassification, Location, Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import Celeste64Item, item_data_table, item_table
from .Locations import Celeste64Location, location_data_table, location_table
from .Names import ItemName
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table, item_table
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
sign_location_data_table, car_location_data_table, location_table
from .Names import ItemName, LocationName
from .Options import Celeste64Options
@@ -27,6 +29,7 @@ class Celeste64World(World):
"""Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer.
Created in a week(ish) by the Celeste team to celebrate the games sixth anniversary 🍓✨"""
# Class Data
game = "Celeste 64"
web = Celeste64WebWorld()
options_dataclass = Celeste64Options
@@ -34,13 +37,18 @@ class Celeste64World(World):
location_name_to_id = location_table
item_name_to_id = item_table
# Instance Data
strawberries_required: int
active_logic_mapping: Dict[str, List[List[str]]]
goal_logic_mapping: Dict[str, List[List[str]]]
def create_item(self, name: str) -> Celeste64Item:
# Only make required amount of strawberries be Progression
if getattr(self, "options", None) and name == ItemName.strawberry:
if getattr(self, "strawberries_required", None) and name == ItemName.strawberry:
classification: ItemClassification = ItemClassification.filler
self.prog_strawberries = getattr(self, "prog_strawberries", 0)
if self.prog_strawberries < self.options.strawberries_required.value:
if self.prog_strawberries < self.strawberries_required:
classification = ItemClassification.progression_skip_balancing
self.prog_strawberries += 1
@@ -51,9 +59,48 @@ class Celeste64World(World):
def create_items(self) -> None:
item_pool: List[Celeste64Item] = []
item_pool += [self.create_item(name) for name in item_data_table.keys()]
location_count: int = 30
item_pool += [self.create_item(ItemName.strawberry) for _ in range(21)]
if self.options.friendsanity:
location_count += 9
if self.options.signsanity:
location_count += 5
if self.options.carsanity:
location_count += 2
item_pool += [self.create_item(name)
for name in unlockable_item_data_table.keys()
if name not in self.options.start_inventory]
if self.options.move_shuffle:
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
if self.options.logic_difficulty == "standard":
# If the start_inventory already includes a move, don't worry about giving it one
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
chosen_start_move = self.random.choice(move_items_for_itempool)
move_items_for_itempool.remove(chosen_start_move)
if self.options.carsanity:
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
intro_car_loc.place_locked_item(self.create_item(chosen_start_move))
location_count -= 1
else:
self.multiworld.push_precollected(self.create_item(chosen_start_move))
item_pool += [self.create_item(name)
for name in move_items_for_itempool
if name not in self.options.start_inventory]
real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - len(item_pool))
self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
item_pool += [self.create_item(ItemName.strawberry) for _ in range(real_total_strawberries)]
filler_item_count: int = location_count - len(item_pool)
item_pool += [self.create_item(ItemName.raspberry) for _ in range(filler_item_count)]
self.multiworld.itempool += item_pool
@@ -69,14 +116,33 @@ class Celeste64World(World):
for region_name, region_data in region_data_table.items():
region = self.multiworld.get_region(region_name, self.player)
region.add_locations({
location_name: location_data.address for location_name, location_data in location_data_table.items()
location_name: location_data.address for location_name, location_data in strawberry_location_data_table.items()
if location_data.region == region_name
}, Celeste64Location)
if self.options.friendsanity:
region.add_locations({
location_name: location_data.address for location_name, location_data in friend_location_data_table.items()
if location_data.region == region_name
}, Celeste64Location)
if self.options.signsanity:
region.add_locations({
location_name: location_data.address for location_name, location_data in sign_location_data_table.items()
if location_data.region == region_name
}, Celeste64Location)
if self.options.carsanity:
region.add_locations({
location_name: location_data.address for location_name, location_data in car_location_data_table.items()
if location_data.region == region_name
}, Celeste64Location)
region.add_exits(region_data_table[region_name].connecting_regions)
def get_filler_item_name(self) -> str:
return ItemName.strawberry
return ItemName.raspberry
def set_rules(self) -> None:
@@ -88,5 +154,12 @@ class Celeste64World(World):
return {
"death_link": self.options.death_link.value,
"death_link_amnesty": self.options.death_link_amnesty.value,
"strawberries_required": self.options.strawberries_required.value
"strawberries_required": self.strawberries_required,
"move_shuffle": self.options.move_shuffle.value,
"friendsanity": self.options.friendsanity.value,
"signsanity": self.options.signsanity.value,
"carsanity": self.options.carsanity.value,
"badeline_chaser_source": self.options.badeline_chaser_source.value,
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
}

View File

@@ -3,6 +3,11 @@
## Required Software
- Archipelago Build of Celeste 64 from: [Celeste 64 Archipelago Releases Page](https://github.com/PoryGoneDev/Celeste64/releases/)
## Optional Software
- Celeste 64 Tracker
- PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/)
- Celeste 64 Archipelago PopTracker pack from: [Celeste 64 AP Tracker Releases Page](https://github.com/PoryGone/Celeste-64-AP-Tracker/releases/)
## Installation Procedures (Windows)
1. Download the above release and extract it.

View File

@@ -14,7 +14,7 @@ from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_st
from .regions import get_region_info
from .rules import CV64Rules
from .data import iname, rname, ename
from ..AutoWorld import WebWorld, World
from worlds.AutoWorld import WebWorld, World
from .aesthetics import randomize_lighting, shuffle_sub_weapons, rom_empty_breakables_flags, rom_sub_weapon_flags, \
randomize_music, get_start_inventory_data, get_location_data, randomize_shop_prices, get_loading_zone_bytes, \
get_countdown_numbers

View File

@@ -521,7 +521,7 @@ rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
options = Utils.get_settings()
executable = options["factorio_options"]["executable"]
server_settings = args.server_settings if args.server_settings \
else options["factorio_options"].get("server_settings", None)

View File

@@ -199,8 +199,14 @@ class HKWorld(World):
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()
if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]:
from .GodhomeData import godhome_event_names
all_event_names.update(set(godhome_event_names))
# Link regions
for event_name in event_names:
for event_name in all_event_names:
#if event_name in wp_exclusions:
# continue
loc = HKLocation(self.player, event_name, None, menu_region)
@@ -307,12 +313,6 @@ class HKWorld(World):
randomized = True
_add("Elevator_Pass", "Elevator_Pass", randomized)
# check for any goal that godhome events are relevant to
if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]:
from .GodhomeData import godhome_event_names
for item_name in godhome_event_names:
_add(item_name, item_name, False)
for shop, locations in self.created_multi_locations.items():
for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value):
loc = self.create_location(shop)

View File

@@ -4,6 +4,7 @@ Archipelago init file for Lingo
from logging import warning
from BaseClasses import Item, ItemClassification, Tutorial
from Options import OptionError
from worlds.AutoWorld import WebWorld, World
from .datatypes import Room, RoomEntrance
from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem
@@ -52,13 +53,14 @@ class LingoWorld(World):
player_logic: LingoPlayerLogic
def generate_early(self):
if not (self.options.shuffle_doors or self.options.shuffle_colors):
if not (self.options.shuffle_doors or self.options.shuffle_colors or self.options.shuffle_sunwarps):
if self.multiworld.players == 1:
warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression"
f" items. Please turn on Door Shuffle or Color Shuffle if that doesn't seem right.")
f" items. Please turn on Door Shuffle, Color Shuffle, or Sunwarp Shuffle if that doesn't seem"
f" right.")
else:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any"
f" progression items. Please turn on Door Shuffle or Color Shuffle.")
raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any"
f" progression items. Please turn on Door Shuffle, Color Shuffle or Sunwarp Shuffle.")
self.player_logic = LingoPlayerLogic(self)
@@ -94,7 +96,7 @@ class LingoWorld(World):
total_weight = sum(self.options.trap_weights.values())
if total_weight == 0:
raise Exception("Sum of trap weights must be at least one.")
raise OptionError("Sum of trap weights must be at least one.")
trap_counts = {name: int(weight * traps / total_weight)
for name, weight in self.options.trap_weights.items()}

View File

@@ -10,6 +10,7 @@ class LocationClassification(Flag):
normal = auto()
reduced = auto()
insanity = auto()
small_sphere_one = auto()
class LocationData(NamedTuple):
@@ -47,6 +48,9 @@ def load_location_data():
if not panel.exclude_reduce:
classification |= LocationClassification.reduced
if room_name == "Starting Room":
classification |= LocationClassification.small_sphere_one
ALL_LOCATION_TABLE[location_name] = \
LocationData(get_panel_location_id(room_name, panel_name), room_name,
[RoomAndPanel(None, panel_name)], classification)

View File

@@ -3,7 +3,7 @@ from dataclasses import dataclass
from schema import And, Schema
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict
from worlds.lingo.items import TRAP_ITEMS
from .items import TRAP_ITEMS
class ShuffleDoors(Choice):

View File

@@ -1,6 +1,7 @@
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
from Options import OptionError
from .datatypes import Door, DoorType, RoomAndDoor, RoomAndPanel
from .items import ALL_ITEM_TABLE, ItemType
from .locations import ALL_LOCATION_TABLE, LocationClassification
@@ -149,8 +150,8 @@ class LingoPlayerLogic:
early_color_hallways = world.options.early_color_hallways
if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none:
raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not "
"be enough locations for all of the door items.")
raise OptionError("You cannot have reduced location checks when door shuffle is on, because there would not"
" be enough locations for all of the door items.")
# Create door items, where needed.
door_groups: Set[str] = set()
@@ -219,7 +220,7 @@ class LingoPlayerLogic:
self.event_loc_to_item[self.level_2_location] = "Victory"
if world.options.level_2_requirement == 1:
raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.")
raise OptionError("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.")
elif victory_condition == VictoryCondition.option_pilgrimage:
self.victory_condition = "Pilgrim Antechamber - PILGRIM"
self.add_location("Pilgrim Antechamber", "PILGRIM (Solved)", None,
@@ -236,20 +237,23 @@ class LingoPlayerLogic:
elif location_checks == LocationChecks.option_insanity:
location_classification = LocationClassification.insanity
if door_shuffle != ShuffleDoors.option_none and not early_color_hallways:
location_classification |= LocationClassification.small_sphere_one
for location_name, location_data in ALL_LOCATION_TABLE.items():
if location_name != self.victory_condition:
if location_classification not in location_data.classification:
if not (location_classification & location_data.classification):
continue
self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world)
self.real_locations.append(location_name)
if world.options.enable_pilgrimage and world.options.sunwarp_access == SunwarpAccess.option_disabled:
raise Exception("Sunwarps cannot be disabled when pilgrimage is enabled.")
raise OptionError("Sunwarps cannot be disabled when pilgrimage is enabled.")
if world.options.shuffle_sunwarps:
if world.options.sunwarp_access == SunwarpAccess.option_disabled:
raise Exception("Sunwarps cannot be shuffled if they are disabled.")
raise OptionError("Sunwarps cannot be shuffled if they are disabled.")
self.sunwarp_mapping = list(range(0, 12))
world.random.shuffle(self.sunwarp_mapping)
@@ -275,7 +279,7 @@ class LingoPlayerLogic:
"iterations. This is very unlikely to happen on its own, and probably indicates some "
"kind of logic error.")
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
if door_shuffle != ShuffleDoors.option_none and location_checks != LocationChecks.option_insanity \
and not early_color_hallways and world.multiworld.players > 1:
# Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is
# only three checks. In a multiplayer situation, this can be frustrating for the player because they are

View File

@@ -78,13 +78,16 @@ def get_progressive_item_id(name: str):
def load_static_data_from_file():
global PAINTING_ENTRANCES, PAINTING_EXITS
from . import datatypes
from Utils import safe_builtins
class RenameUnpickler(pickle.Unpickler):
def find_class(self, module, name):
renamed_module = module
if module == "datatypes":
renamed_module = "worlds.lingo.datatypes"
return super(RenameUnpickler, self).find_class(renamed_module, name)
if module in ("worlds.lingo.datatypes", "datatypes"):
return getattr(datatypes, name)
elif module == "builtins" and name in safe_builtins:
return getattr(safe_builtins, name)
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
file = pkgutil.get_data(__name__, os.path.join("data", "generated.dat"))
pickdata = RenameUnpickler(BytesIO(file)).load()

View File

@@ -1,8 +1,8 @@
import os
import unittest
from worlds.lingo.static_logic import HASHES
from worlds.lingo.utils.pickle_static_data import hash_file
from ..static_logic import HASHES
from ..utils.pickle_static_data import hash_file
class TestDatafile(unittest.TestCase):

View File

@@ -2,8 +2,8 @@ from copy import deepcopy
from typing import List, TYPE_CHECKING
from BaseClasses import CollectionState, PlandoOptions
from worlds.generic import PlandoConnection
from .options import ShufflePortals
from ..generic import PlandoConnection
if TYPE_CHECKING:
from . import MessengerWorld

297
worlds/mlss/Client.py Normal file
View File

@@ -0,0 +1,297 @@
from typing import TYPE_CHECKING, Optional, Set, List, Dict
import struct
from NetUtils import ClientStatus
from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward
from .Items import items_by_id
import asyncio
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
ROOM_ARRAY_POINTER = 0x51FA00
class MLSSClient(BizHawkClient):
game = "Mario & Luigi Superstar Saga"
system = "GBA"
patch_suffix = ".apmlss"
local_checked_locations: Set[int]
goal_flag: int
rom_slot_name: Optional[str]
eUsed: List[int]
room: int
local_events: List[int]
player_name: Optional[str]
checked_flags: Dict[int, list] = {}
def __init__(self) -> None:
super().__init__()
self.local_checked_locations = set()
self.local_set_events = {}
self.local_found_key_items = {}
self.rom_slot_name = None
self.seed_verify = False
self.eUsed = []
self.room = 0
self.local_events = []
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
try:
# Check ROM name/patch version
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8")
if not rom_name.startswith("MARIO&LUIGIU"):
return False
if rom_name == "MARIO&LUIGIUA8":
logger.info(
"ERROR: You appear to be running an unpatched version of Mario & Luigi Superstar Saga. "
"You need to generate a patch file and use it to create a patched ROM."
)
return False
if rom_name != "MARIO&LUIGIUAP":
logger.info(
"ERROR: The patch file used to create this ROM is not compatible with "
"this client. Double check your client version against the version being "
"used by the generator."
)
return False
except UnicodeDecodeError:
return False
except bizhawk.RequestFailedError:
return False # Should verify on the next pass
ctx.game = self.game
ctx.items_handling = 0b101
ctx.want_slot_data = True
ctx.watcher_timeout = 0.125
self.rom_slot_name = rom_name
self.seed_verify = False
name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(0xDF0000, 16, "ROM")]))[0]
name = bytes([byte for byte in name_bytes if byte != 0]).decode("UTF-8")
self.player_name = name
for i in range(59):
self.checked_flags[i] = []
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
ctx.auth = self.player_name
def on_package(self, ctx, cmd, args) -> None:
if cmd == "RoomInfo":
ctx.seed_name = args["seed_name"]
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from CommonClient import logger
try:
if ctx.seed_name is None:
return
if not self.seed_verify:
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
seed = seed[0].decode("UTF-8")
if seed != ctx.seed_name:
logger.info(
"ERROR: The ROM you loaded is for a different game of AP. "
"Please make sure the host has sent you the correct patch file,"
"and that you have opened the correct ROM."
)
raise bizhawk.ConnectorError("Loaded ROM is for Incorrect lobby.")
self.seed_verify = True
read_state = await bizhawk.read(
ctx.bizhawk_ctx,
[
(0x4564, 59, "EWRAM"),
(0x2330, 2, "IWRAM"),
(0x3FE0, 1, "IWRAM"),
(0x304A, 1, "EWRAM"),
(0x304B, 1, "EWRAM"),
(0x304C, 4, "EWRAM"),
(0x3060, 6, "EWRAM"),
(0x4808, 2, "EWRAM"),
(0x4407, 1, "EWRAM"),
(0x2339, 1, "IWRAM"),
]
)
flags = read_state[0]
current_room = int.from_bytes(read_state[1], "little")
shop_init = read_state[2][0]
shop_scroll = read_state[3][0] & 0x1F
is_buy = read_state[4][0] != 0
shop_address = (struct.unpack("<I", read_state[5])[0]) & 0xFFFFFF
logo = bytes([byte for byte in read_state[6] if byte < 0x70]).decode("UTF-8")
received_index = (read_state[7][0] << 8) + read_state[7][1]
cackletta = read_state[8][0] & 0x40
shopping = read_state[9][0] & 0xF
if logo != "MLSSAP":
return
locs_to_send = set()
# Checking shop purchases
if is_buy:
await bizhawk.write(ctx.bizhawk_ctx, [(0x304A, [0x0, 0x0], "EWRAM")])
if shop_address != 0x3C0618 and shop_address != 0x3C0684:
location = shop[shop_address][shop_scroll]
else:
if shop_init & 0x1 != 0:
location = badge[shop_address][shop_scroll]
else:
location = pants[shop_address][shop_scroll]
if location in ctx.server_locations:
locs_to_send.add(location)
# Loop for receiving items. Item is written as an ID into 0x3057.
# ASM reads the ID in a loop and give the player the item before resetting the RAM address to 0x0.
# If RAM address isn't 0x0 yet break out and try again later to give the rest of the items
for i in range(len(ctx.items_received) - received_index):
item_data = items_by_id[ctx.items_received[received_index + i].item]
b = await bizhawk.guarded_read(ctx.bizhawk_ctx, [(0x3057, 1, "EWRAM")], [(0x3057, [0x0], "EWRAM")])
if b is None:
break
await bizhawk.write(
ctx.bizhawk_ctx,
[
(0x3057, [id_to_RAM(item_data.itemID)], "EWRAM"),
(0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"),
],
)
await asyncio.sleep(0.1)
# Early return and location send if you are currently in a shop,
# since other flags aren't going to change
if shopping & 0x3 == 0x3:
if locs_to_send != self.local_checked_locations:
self.local_checked_locations = locs_to_send
if locs_to_send is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
return
# Checking flags that aren't digspots or blocks
for item in nonBlock:
address, mask, location = item
if location in self.local_checked_locations:
continue
flag_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(address, 1, "EWRAM"), (0x3060, 6, "EWRAM")])
flag_byte = flag_bytes[0][0]
backup_logo = bytes([byte for byte in flag_bytes[1] if byte < 0x70]).decode("UTF-8")
if backup_logo != "MLSSAP":
return
if flag_byte & mask != 0:
if location >= 0xDA0000 and location not in self.local_events:
self.local_events += [location]
await ctx.send_msgs(
[
{
"cmd": "Set",
"key": f"mlss_flag_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "or", "value": 1 << (location - 0xDA0000)}],
}
]
)
continue
if location in roomException:
if current_room not in roomException[location]:
exception = True
else:
exception = False
else:
exception = True
if location in eReward:
if location not in self.eUsed:
self.eUsed += [location]
location = eReward[len(self.eUsed) - 1]
else:
continue
if (location in ctx.server_locations) and exception:
locs_to_send.add(location)
# Check for set location flags.
for byte_i, byte in enumerate(bytearray(flags)):
for j in range(8):
if j in self.checked_flags[byte_i]:
continue
and_value = 1 << j
if byte & and_value != 0:
flag_id = byte_i * 8 + (j + 1)
room, item = find_key(roomCount, flag_id)
pointer_arr = await bizhawk.read(
ctx.bizhawk_ctx, [(ROOM_ARRAY_POINTER + ((room - 1) * 4), 4, "ROM")]
)
pointer = struct.unpack("<I", pointer_arr[0])[0]
pointer = pointer & 0xFFFFFF
offset = await bizhawk.read(ctx.bizhawk_ctx, [(pointer, 1, "ROM")])
offset = offset[0][0]
if offset != 0:
offset = 2
pointer += (item * 8) + 1 + offset
for key, value in beanstones.items():
if pointer == value:
pointer = key
break
if pointer in ctx.server_locations:
self.checked_flags[byte_i] += [j]
locs_to_send.add(pointer)
if not ctx.finished_game and cackletta != 0 and current_room == 0x1C7:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
if self.room != current_room:
self.room = current_room
await ctx.send_msgs(
[
{
"cmd": "Set",
"key": f"mlss_room_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "replace", "value": current_room}],
}
]
)
# Send locations if there are any to send.
if locs_to_send != self.local_checked_locations:
self.local_checked_locations = locs_to_send
if locs_to_send is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
except bizhawk.RequestFailedError:
# Exit handler and return to main loop to reconnect.
pass
except bizhawk.ConnectorError:
pass
def find_key(dictionary, target):
leftover = target
for key, value in dictionary.items():
if leftover > value:
leftover -= value
else:
return key, leftover
def id_to_RAM(id_: int):
code = id_
if 0x1C <= code <= 0x1F:
code += 0xE
if 0x20 <= code <= 0x26:
code -= 0x4
return code

5705
worlds/mlss/Data.py Normal file

File diff suppressed because it is too large Load Diff

190
worlds/mlss/Items.py Normal file
View File

@@ -0,0 +1,190 @@
import typing
from BaseClasses import Item, ItemClassification
class ItemData(typing.NamedTuple):
code: int
itemName: str
classification: ItemClassification
itemID: int
class MLSSItem(Item):
game: str = "Mario & Luigi Superstar Saga"
itemList: typing.List[ItemData] = [
ItemData(77771000, "5 Coins", ItemClassification.filler, 0x4),
ItemData(77771001, "Mushroom", ItemClassification.filler, 0xA),
ItemData(77771002, "Super Mushroom", ItemClassification.filler, 0xB),
ItemData(77771003, "Ultra Mushroom", ItemClassification.filler, 0xC),
ItemData(77771004, "Max Mushroom", ItemClassification.filler, 0xD),
ItemData(77771005, "Nuts", ItemClassification.filler, 0xE),
ItemData(77771006, "Super Nuts", ItemClassification.filler, 0xF),
ItemData(77771007, "Ultra Nuts", ItemClassification.useful, 0x10),
ItemData(77771008, "Max Nuts", ItemClassification.useful, 0x11),
ItemData(77771009, "Syrup", ItemClassification.filler, 0x12),
ItemData(77771010, "Super Syrup", ItemClassification.filler, 0x13),
ItemData(77771011, "Ultra Syrup", ItemClassification.useful, 0x14),
ItemData(77771012, "Max Syrup", ItemClassification.useful, 0x15),
ItemData(77771013, "1-UP Mushroom", ItemClassification.useful, 0x16),
ItemData(77771014, "1-UP Super", ItemClassification.useful, 0x17),
ItemData(77771015, "Golden Mushroom", ItemClassification.useful, 0x18),
ItemData(77771016, "Refreshing Herb", ItemClassification.filler, 0x19),
ItemData(77771017, "Red Pepper", ItemClassification.useful, 0x1A),
ItemData(77771018, "Green Pepper", ItemClassification.useful, 0x1B),
ItemData(77771019, "Hoo Bean", ItemClassification.filler, 0x1D),
ItemData(77771020, "Chuckle Bean", ItemClassification.filler, 0x1E),
ItemData(77771021, "Woohoo Blend", ItemClassification.useful, 0x20),
ItemData(77771022, "Hoohoo Blend", ItemClassification.useful, 0x21),
ItemData(77771023, "Chuckle Blend", ItemClassification.useful, 0x22),
ItemData(77771024, "Teehee Blend", ItemClassification.useful, 0x23),
ItemData(77771025, "Hoolumbian", ItemClassification.useful, 0x24),
ItemData(77771026, "Chuckoccino", ItemClassification.useful, 0x25),
ItemData(77771027, "Teeheespresso", ItemClassification.useful, 0x26),
ItemData(77771028, "Peasley's Rose", ItemClassification.progression, 0x31),
ItemData(77771029, "Beanbean Brooch", ItemClassification.progression, 0x32),
ItemData(77771030, "Red Goblet", ItemClassification.progression, 0x33),
ItemData(77771031, "Green Goblet", ItemClassification.progression, 0x34),
ItemData(77771032, "Red Chuckola Fruit", ItemClassification.progression, 0x35),
ItemData(77771033, "White Chuckola Fruit", ItemClassification.progression, 0x36),
ItemData(77771034, "Purple Chuckola Fruit", ItemClassification.progression, 0x37),
ItemData(77771035, "Hammers", ItemClassification.progression, 0x38),
ItemData(77771036, "Firebrand", ItemClassification.progression, 0x39),
ItemData(77771037, "Thunderhand", ItemClassification.progression, 0x3A),
ItemData(77771038, "Membership Card", ItemClassification.progression, 0x40),
ItemData(77771039, "Winkle Card", ItemClassification.progression, 0x41),
ItemData(77771040, "Peach's Extra Dress", ItemClassification.progression, 0x42),
ItemData(77771041, "Fake Beanstar", ItemClassification.progression, 0x43),
ItemData(77771042, "Red Pearl Bean", ItemClassification.progression, 0x45),
ItemData(77771043, "Green Pearl Bean", ItemClassification.progression, 0x46),
ItemData(77771044, "Bean Fruit 1", ItemClassification.progression_skip_balancing, 0x47),
ItemData(77771045, "Bean Fruit 2", ItemClassification.progression_skip_balancing, 0x50),
ItemData(77771046, "Bean Fruit 3", ItemClassification.progression_skip_balancing, 0x51),
ItemData(77771047, "Bean Fruit 4", ItemClassification.progression_skip_balancing, 0x52),
ItemData(77771048, "Bean Fruit 5", ItemClassification.progression_skip_balancing, 0x53),
ItemData(77771049, "Bean Fruit 6", ItemClassification.progression_skip_balancing, 0x54),
ItemData(77771050, "Bean Fruit 7", ItemClassification.progression_skip_balancing, 0x55),
ItemData(77771051, "Blue Neon Egg", ItemClassification.progression, 0x56),
ItemData(77771052, "Red Neon Egg", ItemClassification.progression, 0x57),
ItemData(77771053, "Green Neon Egg", ItemClassification.progression, 0x60),
ItemData(77771054, "Yellow Neon Egg", ItemClassification.progression, 0x61),
ItemData(77771055, "Purple Neon Egg", ItemClassification.progression, 0x62),
ItemData(77771056, "Orange Neon Egg", ItemClassification.progression, 0x63),
ItemData(77771057, "Azure Neon Egg", ItemClassification.progression, 0x64),
ItemData(77771058, "Beanstar Piece 1", ItemClassification.progression, 0x65),
ItemData(77771059, "Beanstar Piece 2", ItemClassification.progression, 0x66),
ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67),
ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70),
ItemData(77771062, "Spangle", ItemClassification.progression, 0x72),
ItemData(77771063, "Beanlet 1", ItemClassification.filler, 0x73),
ItemData(77771064, "Beanlet 2", ItemClassification.filler, 0x74),
ItemData(77771065, "Beanlet 3", ItemClassification.filler, 0x75),
ItemData(77771066, "Beanlet 4", ItemClassification.filler, 0x76),
ItemData(77771067, "Beanlet 5", ItemClassification.filler, 0x77),
ItemData(77771068, "Beanstone 1", ItemClassification.filler, 0x80),
ItemData(77771069, "Beanstone 2", ItemClassification.filler, 0x81),
ItemData(77771070, "Beanstone 3", ItemClassification.filler, 0x82),
ItemData(77771071, "Beanstone 4", ItemClassification.filler, 0x83),
ItemData(77771072, "Beanstone 5", ItemClassification.filler, 0x84),
ItemData(77771073, "Beanstone 6", ItemClassification.filler, 0x85),
ItemData(77771074, "Beanstone 7", ItemClassification.filler, 0x86),
ItemData(77771075, "Beanstone 8", ItemClassification.filler, 0x87),
ItemData(77771076, "Beanstone 9", ItemClassification.filler, 0x90),
ItemData(77771077, "Beanstone 10", ItemClassification.filler, 0x91),
ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92),
ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93),
ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F),
ItemData(77771081, "Pea Badge", ItemClassification.useful, 0xA0),
ItemData(77771082, "Bean B. Badge", ItemClassification.useful, 0xA1),
ItemData(77771083, "Counter Badge", ItemClassification.useful, 0xA2),
ItemData(77771084, "Charity Badge", ItemClassification.useful, 0xA3),
ItemData(77771085, "Bros. Badge", ItemClassification.useful, 0xA4),
ItemData(77771086, "Miracle Badge", ItemClassification.useful, 0xA5),
ItemData(77771087, "Ohoracle Badge", ItemClassification.useful, 0xA6),
ItemData(77771088, "Mush Badge", ItemClassification.useful, 0xA7),
ItemData(77771089, "Mari-Lui Badge", ItemClassification.useful, 0xA8),
ItemData(77771090, "Muscle Badge", ItemClassification.useful, 0xA9),
ItemData(77771091, "Spiny Badge AA", ItemClassification.useful, 0xAA),
ItemData(77771092, "Mush Badge A", ItemClassification.useful, 0xAB),
ItemData(77771093, "Grab Badge", ItemClassification.useful, 0xAC),
ItemData(77771094, "Mush Badge AA", ItemClassification.useful, 0xAD),
ItemData(77771095, "Power Badge", ItemClassification.useful, 0xAE),
ItemData(77771096, "Wonder Badge", ItemClassification.useful, 0xAF),
ItemData(77771097, "Beauty Badge", ItemClassification.useful, 0xB0),
ItemData(77771098, "Salvage Badge", ItemClassification.useful, 0xB1),
ItemData(77771099, "Oh-Pah Badge", ItemClassification.useful, 0xB2),
ItemData(77771100, "Brilliant Badge", ItemClassification.useful, 0xB3),
ItemData(77771101, "Sarge Badge", ItemClassification.useful, 0xB4),
ItemData(77771102, "General Badge", ItemClassification.useful, 0xB5),
ItemData(77771103, "Tank Badge", ItemClassification.useful, 0xB6),
ItemData(77771104, "Bros. Rock", ItemClassification.useful, 0xBD),
ItemData(77771105, "Soulful Bros.", ItemClassification.useful, 0xC0),
ItemData(77771106, "High-End Badge", ItemClassification.useful, 0xC1),
ItemData(77771107, "Bean Pants", ItemClassification.useful, 0xCC),
ItemData(77771108, "Bean Trousers", ItemClassification.useful, 0xCD),
ItemData(77771109, "Blue Jeans", ItemClassification.useful, 0xCE),
ItemData(77771110, "Parasol Pants", ItemClassification.useful, 0xCF),
ItemData(77771111, "Hard Pants", ItemClassification.useful, 0xD0),
ItemData(77771112, "Heart Jeans", ItemClassification.useful, 0xD1),
ItemData(77771113, "Plaid Trousers", ItemClassification.useful, 0xD2),
ItemData(77771114, "#1 Trousers", ItemClassification.useful, 0xD3),
ItemData(77771115, "Safety Slacks", ItemClassification.useful, 0xD4),
ItemData(77771116, "Shroom Pants", ItemClassification.useful, 0xD5),
ItemData(77771117, "Shroom Bells", ItemClassification.useful, 0xD6),
ItemData(77771118, "Shroom Slacks", ItemClassification.useful, 0xD7),
ItemData(77771119, "Peachy Jeans", ItemClassification.useful, 0xD8),
ItemData(77771120, "Mushwin Pants", ItemClassification.useful, 0xD9),
ItemData(77771121, "Mushluck Pants", ItemClassification.useful, 0xDA),
ItemData(77771122, "Scandal Jeans", ItemClassification.useful, 0xDB),
ItemData(77771123, "Street Jeans", ItemClassification.useful, 0xDC),
ItemData(77771124, "Tropic Slacks", ItemClassification.useful, 0xDD),
ItemData(77771125, "Hermetic Pants", ItemClassification.useful, 0xDE),
ItemData(77771126, "Beanstar Pants", ItemClassification.useful, 0xDF),
ItemData(77771127, "Peasley Slacks", ItemClassification.useful, 0xE0),
ItemData(77771128, "Queen B. Jeans", ItemClassification.useful, 0xE1),
ItemData(77771129, "B. Brand Jeans", ItemClassification.useful, 0xE2),
ItemData(77771130, "Heart Slacks", ItemClassification.useful, 0xE3),
ItemData(77771131, "Casual Slacks", ItemClassification.useful, 0xE4),
ItemData(77771132, "Casual Coral", ItemClassification.useful, 0xEB),
ItemData(77771133, "Harhall's Pants", ItemClassification.useful, 0xF1),
ItemData(77771134, "Wool Trousers", ItemClassification.useful, 0xF3),
ItemData(77771135, "Iron Pants", ItemClassification.useful, 0xF7),
ItemData(77771136, "Greed Wallet", ItemClassification.useful, 0xF8),
ItemData(77771137, "Bonus Ring", ItemClassification.useful, 0xF9),
ItemData(77771138, "Excite Spring", ItemClassification.useful, 0xFA),
ItemData(77771139, "Great Force", ItemClassification.useful, 0xFB),
ItemData(77771140, "Power Grip", ItemClassification.useful, 0xFC),
ItemData(77771141, "Cobalt Necktie", ItemClassification.useful, 0xFD),
ItemData(77771142, "Game Boy Horror SP", ItemClassification.useful, 0xFE),
ItemData(77771143, "Woo Bean", ItemClassification.skip_balancing, 0x1C),
ItemData(77771144, "Hee Bean", ItemClassification.skip_balancing, 0x1F),
]
item_frequencies: typing.Dict[str, int] = {
"5 Coins": 40,
"Mushroom": 55,
"Super Mushroom": 15,
"Ultra Mushroom": 12,
"Nuts": 10,
"Super Nuts": 5,
"Ultra Nuts": 5,
"Max Nuts": 2,
"Syrup": 28,
"Super Syrup": 10,
"Ultra Syrup": 10,
"Max Syrup": 2,
"1-Up Mushroom": 15,
"1-Up Super": 5,
"Golden Mushroom": 3,
"Refreshing Herb": 9,
"Red Pepper": 2,
"Green Pepper": 2,
"Hoo Bean": 100,
"Chuckle Bean": 200,
"Hammers": 3,
}
item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in itemList}
items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in itemList}

1185
worlds/mlss/Locations.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,559 @@
class LocationName:
StardustFields1Block1 = "Stardust Fields Room 1 Block 1"
StardustFields1Block2 = "Stardust Fields Room 1 Block 2"
StardustFields2Block = "Stardust Fields Room 2 Block"
StardustFields3Block = "Stardust Fields Room 3 Block"
StardustFields4Block1 = "Stardust Fields Room 4 Block 1"
StardustFields4Block2 = "Stardust Fields Room 4 Block 2"
StardustFields4Block3 = "Stardust Fields Room 4 Block 3"
StardustFields5Block = "Stardust Fields Room 5 Block"
HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block"
BeanbeanCastleTownLeftSideHouseBlock1 = "Beanbean Castle Town Left Side House Block 1"
BeanbeanCastleTownLeftSideHouseBlock2 = "Beanbean Castle Town Left Side House Block 2"
BeanbeanCastleTownLeftSideHouseBlock3 = "Beanbean Castle Town Left Side House Block 3"
BeanbeanCastleTownLeftSideHouseBlock4 = "Beanbean Castle Town Left Side House Block 4"
BeanbeanCastleTownRightSideHouseBlock1 = "Beanbean Castle Town Right Side House Block 1"
BeanbeanCastleTownRightSideHouseBlock2 = "Beanbean Castle Town Right Side House Block 2"
BeanbeanCastleTownRightSideHouseBlock3 = "Beanbean Castle Town Right Side House Block 3"
BeanbeanCastleTownRightSideHouseBlock4 = "Beanbean Castle Town Right Side House Block 4"
BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1"
BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2"
BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3"
BeanbeanCastleTownMiniMarioBlock4 = "Beanbean Castle Town Mini Mario Block 4"
BeanbeanCastleTownMiniMarioBlock5 = "Beanbean Castle Town Mini Mario Block 5"
HoohooMountainSummitDigspot = "Hoohoo Mountain Summit Digspot"
HoohooMountainBelowSummitDigspot = "Hoohoo Mountain Below Summit Digspot"
HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1"
HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2"
HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3"
HoohooMountainAfterHoohoorosBlock1 = "Hoohoo Mountain After Hoohooros Block 1"
HoohooMountainAfterHoohoorosDigspot = "Hoohoo Mountain After Hoohooros Digspot"
HoohooMountainAfterHoohoorosBlock2 = "Hoohoo Mountain After Hoohooros Block 2"
HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1"
HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2"
HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1"
HoohooMountainHoohoorosRoomDigspot2 = "Hoohoo Mountain Hoohooros Room Digspot 2"
HoohooMountainBeforeHoohoorosBlock = "Hoohoo Mountain Before Hoohooros Block"
HoohooMountainBeforeHoohoorosDigspot = "Hoohoo Mountain Before Hoohooros Digspot"
HoohooMountainFountainRoomBlock1 = "Hoohoo Mountain Fountain Room Block 1"
HoohooMountainFountainRoomBlock2 = "Hoohoo Mountain Fountain Room Block 2"
HoohooMountainRoom2Digspot1 = "Hoohoo Mountain Room 2 Digspot 1"
HoohooMountainRoom2Digspot2 = "Hoohoo Mountain Room 2 Digspot 2"
HoohooMountainRoom1Block1 = "Hoohoo Mountain Room 1 Block 1"
HoohooMountainRoom1Block2 = "Hoohoo Mountain Room 1 Block 2"
HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3"
HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block"
HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot"
HoohooVillageRightSideBlock = "Hoohoo Village Right Side Block"
HoohooVillageRightSideDigspot = "Hoohoo Village Right Side Digspot"
HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1"
HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2"
HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3"
HoohooMountainBaseBridgeRoomBlock1 = "Hoohoo Mountain Base Bridge Room Block 1"
HoohooMountainBaseBridgeRoomBlock2 = "Hoohoo Mountain Base Bridge Room Block 2"
HoohooMountainBaseBridgeRoomBlock3 = "Hoohoo Mountain Base Bridge Room Block 3"
HoohooMountainBaseBridgeRoomBlock4 = "Hoohoo Mountain Base Bridge Room Block 4"
HoohooMountainBaseBridgeRoomDigspot = "Hoohoo Mountain Base Bridge Room Digspot"
HoohooMountainBaseBoostatueRoomBlock1 = "Hoohoo Mountain Base Boostatue Room Block 1"
HoohooMountainBaseBoostatueRoomBlock2 = "Hoohoo Mountain Base Boostatue Room Block 2"
HoohooMountainBaseBoostatueRoomDigspot1 = "Hoohoo Mountain Base Boostatue Room Digspot 1"
HoohooMountainBaseBoostatueRoomDigspot2 = "Hoohoo Mountain Base Boostatue Room Digspot 2"
HoohooMountainBaseBoostatueRoomDigspot3 = "Hoohoo Mountain Base Boostatue Room Digspot 3"
BeanbeanOutskirtsBooStatueMole = "Beanbean Outskirts Boo Statue Mole"
HoohooMountainBaseGrassyAreaBlock1 = "Hoohoo Mountain Base Grassy Area Block 1"
HoohooMountainBaseGrassyAreaBlock2 = "Hoohoo Mountain Base Grassy Area Block 2"
HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot"
HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot"
HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block"
HoohooMountainBaseAfterMinecartMinigameBlock1 = "Hoohoo Mountain Base After Minecart Minigame Block 1"
HoohooMountainBaseAfterMinecartMinigameBlock2 = "Hoohoo Mountain Base After Minecart Minigame Block 2"
HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1"
HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2"
HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3"
CaveConnectingStardustFieldsAndHoohooVillageBlock1 = "Cave Connecting Stardust Fields and Hoohoo Village Block 1"
CaveConnectingStardustFieldsAndHoohooVillageBlock2 = "Cave Connecting Stardust Fields and Hoohoo Village Block 2"
HoohooVillageSouthCaveBlock = "Hoohoo Village South Cave Block"
HoohooVillageSuperHammerCaveDigspot = "Hoohoo Village Super Hammer Cave Digspot"
HoohooVillageSuperHammerCaveBlock = "Hoohoo Village Super Hammer Cave Block"
HoohooVillageNorthCaveRoom1Block = "Hoohoo Village North Cave Room 1 Block"
HoohooVillageNorthCaveRoom2Block = "Hoohoo Village North Cave Room 2 Block"
HoohooVillageNorthCaveRoom2Digspot = "Hoohoo Village North Cave Room 2 Digspot"
HoohooMountainBaseMinecartCaveDigspot = "Hoohoo Mountain Base Minecart Cave Digspot"
BeanbeanOutskirtsFarmRoomDigspot1 = "Beanbean Outskirts Farm Room Digspot 1"
BeanbeanOutskirtsFarmRoomDigspot2 = "Beanbean Outskirts Farm Room Digspot 2"
BeanbeanOutskirtsFarmRoomDigspot3 = "Beanbean Outskirts Farm Room Digspot 3"
BeanbeanOutskirtsNWBlock = "Beanbean Outskirts NW Block"
BeanbeanOutskirtsNWDigspot = "Beanbean Outskirts NW Digspot"
BeanbeanOutskirtsWDigspot1 = "Beanbean Outskirts W Digspot 1"
BeanbeanOutskirtsWDigspot2 = "Beanbean Outskirts W"
BeanbeanOutskirtsNRoom1Digspot = "Beanbean Outskirts N Room 1 Digspot"
BeanbeanOutskirtsNRoom2Digspot = "Beanbean Outskirts N Room 2 Digspot"
BeanbeanOutskirtsSRoom1Digspot1 = "Beanbean Outskirts S Room 1 Digspot 1"
BeanbeanOutskirtsSRoom1Block = "Beanbean Outskirts S Room 1 Block"
BeanbeanOutskirtsSRoom1Digspot2 = "Beanbean Outskirts S Room 1 Digspot 2"
BeanbeanOutskirtsSRoom2Block1 = "Beanbean Outskirts S Room 2 Block 1"
BeanbeanOutskirtsSRoom2Digspot1 = "Beanbean Outskirts S Room 2 Digspot 1"
BeanbeanOutskirtsSRoom2Digspot2 = "Beanbean Outskirts S Room 2 Digspot 2"
BeanbeanOutskirtsSRoom2Block2 = "Beanbean Outskirts S Room 2 Block 2"
BeanbeanOutskirtsSRoom2Digspot3 = "Beanbean Outskirts S Room 2 Digspot 3"
BeanbeanOutskirtsNEDigspot1 = "Beanbean Outskirts NE Digspot 1"
BeanbeanOutskirtsNEDigspot2 = "Beanbean Outskirts NE Digspot 2"
BeanbeanOutskirtsEDigspot1 = "Beanbean Outskirts E Digspot 1"
BeanbeanOutskirtsEDigspot2 = "Beanbean Outskirts E Digspot 2"
BeanbeanOutskirtsEDigspot3 = "Beanbean Outskirts E Digspot 3"
BeanbeanOutskirtsSEDigspot1 = "Beanbean Outskirts SE Digspot 1"
BeanbeanOutskirtsSEDigspot2 = "Beanbean Outskirts SE Digspot 2"
BeanbeanOutskirtsSEDigspot3 = "Beanbean Outskirts SE Digspot 3"
BeanbeanOutskirtsNorthBeachDigspot1 = "Beanbean Outskirts North Beach Digspot 1"
BeanbeanOutskirtsNorthBeachDigspot2 = "Beanbean Outskirts North Beach Digspot 2"
BeanbeanOutskirtsNorthBeachDigspot3 = "Beanbean Outskirts North Beach Digspot 3"
BeanbeanOutskirtsSouthBeachDigspot = "Beanbean Outskirts South Beach Digspot"
BeanbeanOutskirtsSurfBeachDigspot1 = "Beanbean Outskirts Surf Beach Digspot 1"
BeanbeanOutskirtsSurfBeachBlock = "Beanbean Outskirts Surf Beach Block"
BeanbeanOutskirtsSurfBeachDigspot2 = "Beanbean Outskirts Surf Beach Digspot 2"
BeanbeanOutskirtsSurfBeachDigspot3 = "Beanbean Outskirts Surf Beach Digspot 3"
ChateauRoom1Digspot = "Chateau Room 1 Digspot"
ChateauPoppleFightRoomBlock1 = "Chateau Popple Fight Room Block 1"
ChateauPoppleFightRoomBlock2 = "Chateau Popple Fight Room Block 2"
ChateauPoppleFightRoomDigspot = "Chateau Popple Fight Room Digspot"
ChateauBarrelRoomDigspot = "Chateau Barrel Room Digspot"
ChateauGobletRoomDigspot = "Chateau Goblet Room Digspot"
ChucklehuckWoodsCaveRoom1Block1 = "Chucklehuck Woods Cave Room 1 Block 1"
ChucklehuckWoodsCaveRoom1Block2 = "Chucklehuck Woods Cave Room 1 Block 2"
ChucklehuckWoodsCaveRoom2Block = "Chucklehuck Woods Cave Room 2 Block"
ChucklehuckWoodsCaveRoom3Block = "Chucklehuck Woods Cave Room 3 Block"
ChucklehuckWoodsRoom2Block = "Chucklehuck Woods Room 2 Block"
ChucklehuckWoodsRoom2Digspot = "Chucklehuck Woods Room 2 Digspot"
ChucklehuckWoodsPipeRoomBlock1 = "Chucklehuck Woods Pipe Room Block 1"
ChucklehuckWoodsPipeRoomBlock2 = "Chucklehuck Woods Pipe Room Block 2"
ChucklehuckWoodsPipeRoomDigspot1 = "Chucklehuck Woods Pipe Room Digspot 1"
ChucklehuckWoodsPipeRoomDigspot2 = "Chucklehuck Woods Pipe Room Digspot 2"
ChucklehuckWoodsRoom4Block1 = "Chucklehuck Woods Room 4 Block 1"
ChucklehuckWoodsRoom4Block2 = "Chucklehuck Woods Room 4 Block 2"
ChucklehuckWoodsRoom4Block3 = "Chucklehuck Woods Room 4 Block 3"
ChucklehuckWoodsRoom7Block1 = "Chucklehuck Woods Room 7 Block 1"
ChucklehuckWoodsRoom7Block2 = "Chucklehuck Woods Room 7 Block 2"
ChucklehuckWoodsRoom7Digspot1 = "Chucklehuck Woods Room 7 Digspot 1"
ChucklehuckWoodsRoom7Digspot2 = "Chucklehuck Woods Room 7 Digspot 2"
ChucklehuckWoodsRoom8Digspot = "Chucklehuck Woods Room 8 Digspot"
ChucklehuckWoodsEastOfChucklerootDigspot = "Chucklehuck Woods East of Chuckleroot Digspot"
ChucklehuckWoodsNortheastOfChucklerootDigspot1 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 1"
ChucklehuckWoodsNortheastOfChucklerootDigspot2 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 2"
ChucklehuckWoodsNortheastOfChucklerootDigspot3 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 3"
ChucklehuckWoodsNortheastOfChucklerootDigspot4 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 4"
ChucklehuckWoodsWhiteFruitRoomDigspot1 = "Chucklehuck Woods White Fruit Room Digspot 1"
ChucklehuckWoodsWhiteFruitRoomDigspot2 = "Chucklehuck Woods White Fruit Room Digspot 2"
ChucklehuckWoodsWhiteFruitRoomDigspot3 = "Chucklehuck Woods White Fruit Room Digspot 3"
ChucklehuckWoodsWestOfChucklerootBlock = "Chucklehuck Woods West of Chuckleroot Block"
ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block"
ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1"
ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2"
ChucklehuckWoodsAfterChucklerootBlock1 = "Chucklehuck Woods After Chuckleroot Block 1"
ChucklehuckWoodsAfterChucklerootBlock2 = "Chucklehuck Woods After Chuckleroot Block 2"
ChucklehuckWoodsAfterChucklerootBlock3 = "Chucklehuck Woods After Chuckleroot Block 3"
ChucklehuckWoodsAfterChucklerootBlock4 = "Chucklehuck Woods After Chuckleroot Block 4"
ChucklehuckWoodsAfterChucklerootBlock5 = "Chucklehuck Woods After Chuckleroot Block 5"
ChucklehuckWoodsAfterChucklerootBlock6 = "Chucklehuck Woods After Chuckleroot Block 6"
WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block"
WinkleAreaDigspot = "Winkle Area Digspot"
WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block"
ChucklehuckWoodsKoopaRoomBlock1 = "Chucklehuck Woods Koopa Room Block 1"
ChucklehuckWoodsKoopaRoomBlock2 = "Chucklehuck Woods Koopa Room Block 2"
ChucklehuckWoodsKoopaRoomDigspot = "Chucklehuck Woods Koopa Room Digspot"
ChucklehuckWoodsWinkleCaveBlock1 = "Chucklehuck Woods Winkle Cave Block 1"
ChucklehuckWoodsWinkleCaveBlock2 = "Chucklehuck Woods Winkle Cave Block 2"
OhoOasisWestDigspot = "Oho Oasis West Digspot"
OhoOasisFirePalaceBlock = "Oho Oasis Fire Palace Block"
SewersRoom3Block1 = "Sewers Room 3 Block 1"
SewersRoom3Block2 = "Sewers Room 3 Block 2"
SewersRoom3Block3 = "Sewers Room 3 Block 3"
SewersRoom5Block1 = "Sewers Room 5 Block 1"
SewersRoom5Block2 = "Sewers Room 5 Block 2"
SewersPrisonRoomBlock1 = "Sewers Prison Room Block 1"
SewersPrisonRoomBlock2 = "Sewers Prison Room Block 2"
SewersPrisonRoomBlock3 = "Sewers Prison Room Block 3"
SewersPrisonRoomBlock4 = "Sewers Prison Room Block 4"
OhoOceanFirePuzzleRoomDigspot = "Oho Ocean Fire Puzzle Room Digspot"
OhoOceanSouthRoom1Block = "Oho Ocean South Room 1 Block"
OhoOceanSouthRoom2Digspot = "Oho Ocean South Room 2 Digspot"
OhoOceanSpikeRoomDigspot1 = "Oho Ocean Spike Room Digspot 1"
OhoOceanSpikeRoomDigspot2 = "Oho Ocean Spike Room Digspot 2"
OceanNorthWhirlpoolBlock1 = "Oho Ocean North Whirlpool Block 1"
OceanNorthWhirlpoolBlock2 = "Oho Ocean North Whirlpool Block 2"
OceanNorthWhirlpoolBlock3 = "Oho Ocean North Whirlpool Block 3"
OceanNorthWhirlpoolBlock4 = "Oho Ocean North Whirlpool Block 4"
OceanNorthWhirlpoolDigspot1 = "Oho Ocean North Whirlpool Digspot 1"
OceanNorthWhirlpoolDigspot2 = "Oho Ocean North Whirlpool Digspot 2"
OceanSouthWhirlpoolDigspot1 = "Oho Ocean South Whirlpool Digspot 1"
OceanSouthWhirlpoolDigspot2 = "Oho Ocean South Whirlpool Digspot 2"
OceanSouthWhirlpoolDigspot3 = "Oho Ocean South Whirlpool Digspot 3"
OceanSouthWhirlpoolDigspot4 = "Oho Ocean South Whirlpool Digspot 4"
OceanSouthWhirlpoolDigspot5 = "Oho Ocean South Whirlpool Digspot 5"
OceanSouthWhirlpoolDigspot6 = "Oho Ocean South Whirlpool Digspot 6"
OceanSouthWhirlpoolRoom2Digspot = "Oho Ocean South Whirlpool Room 2 Digspot"
WoohooHooniversityStarRoomBlock1 = "Woohoo Hooniversity Star Room Block 1"
WoohooHooniversityStarRoomBlock2 = "Woohoo Hooniversity Star Room Block 2"
WoohooHooniversityStarRoomBlock3 = "Woohoo Hooniversity Star Room Block 3"
WoohooHooniversitySunDoorBlock1 = "Woohoo Hooniversity Sun Door Block 1"
WoohooHooniversitySunDoorBlock2 = "Woohoo Hooniversity Sun Door Block 2"
WoohooHooniversitySouthOfStarRoomBlock = "Woohoo Hooniversity South Of Star Room Block"
WoohooHooniversityWestOfStarRoomDigspot1 = "Woohoo Hooniversity West Of Star Room Digspot 1"
WoohooHooniversityWestOfStarRoomDigspot2 = "Woohoo Hooniversity West Of Star Room Digspot 2"
WoohooHooniversityBarrelPuzzleEntranceDigspot1 = "Woohoo Hooniversity Barrel Puzzle Entrance Digspot 1"
WoohooHooniversityBarrelPuzzleEntranceBlock1 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 1"
WoohooHooniversityBarrelPuzzleEntranceBlock2 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 2"
WoohooHooniversityBarrelPuzzleEntranceBlock3 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 3"
WoohooHooniversityBarrelPuzzleEntranceBlock4 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 4"
WoohooHooniversityBarrelPuzzleEntranceDigspot2 = "Woohoo Hooniversity Barrel Puzzle Entrance Digspot 2"
ChucklehuckWoodsRoom1Digspot = "Chucklehuck Woods Room 1 Digspot"
WoohooHooniversityWestOfStarRoom2Digspot = "Woohoo Hooniversity West of Star Room 2 Digspot"
WoohooHooniversityWestOfStarRoom3Digspot = "Woohoo Hooniversity West of Star Room 3 Digspot"
WoohooHooniversityWestOfStarRoom4Block1 = "Woohoo Hooniversity West of Star Room 4 Block 1"
WoohooHooniversityWestOfStarRoom4Block2 = "Woohoo Hooniversity West of Star Room 4 Block 2"
WoohooHooniversityWestOfStarRoom4Block3 = "Woohoo Hooniversity West of Star Room 4 Block 3"
WoohooHooniversityWestOfStarRoom4Digspot1 = "Woohoo Hooniversity West of Star Room 4 Digspot 1"
WoohooHooniversityWestOfStarRoom4Digspot2 = "Woohoo Hooniversity West of Star Room 4 Digspot 2"
WoohooHooniversityWestOfStarRoom5Digspot = "Woohoo Hooniversity West of Star Room 5 Digspot"
WoohooHooniversityEntranceToMiniMarioRoomDigspot1 = "Woohoo Hooniversity Entrance to Mini Mario Room Digspot 1"
WoohooHooniversityEntranceToMiniMarioRoomDigspot2 = "Woohoo Hooniversity Entrance to Mini Mario Room Digspot 2"
WoohooHooniversityEntranceToMiniMarioRoom2Digspot = "Woohoo Hooniversity Entrance to Mini Mario Room 2 Digspot"
WoohooHooniversityMiniMarioPuzzleBlock = "Woohoo Hooniversity Mini Mario Puzzle Block"
WoohooHooniversityMiniMarioPuzzleDigspot = "Woohoo Hooniversity Mini Mario Puzzle Digspot"
WoohooHooniversityMiniMarioPuzzleSecretAreaBlock1 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 1"
WoohooHooniversityMiniMarioPuzzleSecretAreaBlock2 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 2"
WoohooHooniversityMiniMarioPuzzleSecretAreaBlock3 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 3"
WoohooHooniversityMiniMarioPuzzleSecretAreaBlock4 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 4"
WoohooHooniversityPastSunDoorBlock1 = "Woohoo Hooniversity Past Sun Door Block 1"
WoohooHooniversityPastSunDoorBlock2 = "Woohoo Hooniversity Past Sun Door Block 2"
WoohooHooniversityPastSunDoorBlock3 = "Woohoo Hooniversity Past Sun Door Block 3"
WoohooHooniversityPastCacklettaRoom1Block = "Woohoo Hooniversity Past Cackletta Room 1 Block"
WoohooHooniversityPastCacklettaRoom2Block1 = "Woohoo Hooniversity Past Cackletta Room 2 Block 1"
WoohooHooniversityPastCacklettaRoom2Block2 = "Woohoo Hooniversity Past Cackletta Room 2 Block 2"
WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot"
AirportEntranceDigspot = "Airport Entrance Digspot"
AirportLobbyDigspot = "Airport Lobby Digspot"
AirportLeftsideDigspot1 = "Airport Leftside Digspot 1"
AirportLeftsideDigspot2 = "Airport Leftside Digspot 2"
AirportLeftsideDigspot3 = "Airport Leftside Digspot 3"
AirportLeftsideDigspot4 = "Airport Leftside Digspot 4"
AirportLeftsideDigspot5 = "Airport Leftside Digspot 5"
AirportCenterDigspot1 = "Airport Center Digspot 1"
AirportCenterDigspot2 = "Airport Center Digspot 2"
AirportCenterDigspot3 = "Airport Center Digspot 3"
AirportCenterDigspot4 = "Airport Center Digspot 4"
AirportCenterDigspot5 = "Airport Center Digspot 5"
AirportRightsideDigspot1 = "Airport Rightside Digspot 1"
AirportRightsideDigspot2 = "Airport Rightside Digspot 2"
AirportRightsideDigspot3 = "Airport Rightside Digspot 3"
AirportRightsideDigspot4 = "Airport Rightside Digspot 4"
AirportRightsideDigspot5 = "Airport Rightside Digspot 5"
GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot"
GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot"
GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot"
GwarharLagoonEntranceToWestUnderwaterAreaDigspot = "Gwarhar Lagoon Entrance to West Underwater Area Digspot"
GwarharLagoonFireDashPuzzleRoom1Digspot1 = "Gwarhar Lagoon Fire Dash Puzzle Room 1 Digspot 1"
GwarharLagoonFireDashPuzzleRoom1Digspot2 = "Gwarhar Lagoon Fire Dash Puzzle Room 1 Digspot 2"
GwarharLagoonFireDashPuzzleRoom2Digspot = "Gwarhar Lagoon Fire Dash Puzzle Room 2 Digspot"
GwarharLagoonFireDashPuzzleRoom3Digspot1 = "Gwarhar Lagoon Fire Dash Puzzle Room 3 Digspot 1"
GwarharLagoonFireDashPuzzleRoom3Digspot2 = "Gwarhar Lagoon Fire Dash Puzzle Room 3 Digspot 2"
GwarharLagoonEastOfStoneBridgeBlock = "Gwarhar Lagoon East of Stone Bridge Block"
GwarharLagoonNorthOfSpangleRoomDigspot = "Gwarhar Lagoon North of Spangle Room Digspot"
GwarharLagoonWestOfSpangleRoomDigspot = "Gwarhar Lagoon West of Spangle Room Digspot"
GwarharLagoonSpangleRoomBlock = "Gwarhar Lagoon Spangle Room Block"
GwarharLagoonFirstUnderwaterAreaRoom1Block = "Gwarhar Lagoon First Underwater Area Room 1 Block"
GwarharLagoonFirstUnderwaterAreaRoom2Block1 = "Gwarhar Lagoon First Underwater Area Room 2 Block 1"
GwarharLagoonFirstUnderwaterAreaRoom2Block2 = "Gwarhar Lagoon First Underwater Area Room 2 Block 2"
GwarharLagoonSecondUnderwaterAreaRoom4Digspot = "Gwarhar Lagoon Second Underwater Area Room 4 Digspot"
GwarharLagoonSecondUnderwaterAreaRoom2Digspot1 = "Gwarhar Lagoon Second Underwater Area Room 2 Digspot 1"
GwarharLagoonSecondUnderwaterAreaRoom2Digspot2 = "Gwarhar Lagoon Second Underwater Area Room 2 Digspot 2"
GwarharLagoonSecondUnderwaterAreaRoom3Block1 = "Gwarhar Lagoon Second Underwater Area Room 3 Block 1"
GwarharLagoonSecondUnderwaterAreaRoom3Block2 = "Gwarhar Lagoon Second Underwater Area Room 3 Block 2"
GwarharLagoonSecondUnderwaterAreaRoom3Block3 = "Gwarhar Lagoon Second Underwater Area Room 3 Block 3"
GwarharLagoonSecondUnderwaterAreaRoom1Digspot = "Gwarhar Lagoon Second Underwater Area Room 1 Digspot"
WoohooHooniversityBasementRoom1Digspot = "Woohoo Hooniversity Basement Room 1 Digspot"
WoohooHooniversityBasementRoom2Digspot = "Woohoo Hooniversity Basement Room 2 Digspot"
WoohooHooniversityBasementRoom3Block = "Woohoo Hooniversity Basement Room 3 Block"
WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block"
WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1"
WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2"
TeeheeValleyBeforePoppleDigspot1 = "Teehee Valley Before Popple Digspot 1"
TeeheeValleyBeforePoppleDigspot2 = "Teehee Valley Before Popple Digspot 2"
TeeheeValleyBeforePoppleDigspot3 = "Teehee Valley Before Popple Digspot 3"
TeeheeValleyBeforePoppleDigspot4 = "Teehee Valley Before Popple Digspot 4"
TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1"
TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2"
TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3"
TeeheeValleyEastRoomDigspot1 = "Teehee Valley East Room Digspot 1"
TeeheeValleyEastRoomDigspot2 = "Teehee Valley East Room Digspot 2"
TeeheeValleyEastRoomDigspot3 = "Teehee Valley East Room Digspot 3"
TeeheeValleySoloMarioRoomDigspot1 = "Teehee Valley Solo Mario Room Digspot 1"
TeeheeValleySoloMarioRoomDigspot2 = "Teehee Valley Solo Mario Room Digspot 2"
TeeheeValleySoloMarioRoomDigspot3 = "Teehee Valley Solo Mario Room Digspot 3"
TeeheeValleySoloMarioRoomDigspot4 = "Teehee Valley Solo Mario Room Digspot 4"
TeeheeValleyPastUltraHammersBlock1 = "Teehee Valley Past Ultra Hammer Rock Block 1"
TeeheeValleyPastUltraHammersBlock2 = "Teehee Valley Past Ultra Hammer Rock Block 2"
TeeheeValleyPastUltraHammersDigspot1 = "Teehee Valley Past Ultra Hammer Rock Digspot 1"
TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)"
TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3"
TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot"
TeeheeValleySoloLuigiMazeRoom2Digspot1 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 1"
TeeheeValleySoloLuigiMazeRoom2Digspot2 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 2"
TeeheeValleySoloLuigiMazeRoom1Block = "Teehee Valley Solo Luigi Maze Room 1 Block"
TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot"
TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot"
SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1"
SSChuckolaStorageRoomBlock2 = "S.S. Chuckola Storage Room Block 2"
LittleFungitownEmbassyRoomBlock = "Little Fungitown Embassy Room Block"
LittleFungitownEntranceRoomBlock = "Little Fungitown Entrance Room Block"
JokesEndPipeDigspot = "Joke's End Pipe Digspot"
JokesEndStaircaseDigspot = "Joke's End Staircase Digspot"
JokesEndWestOfFirstBoilerRoomBlock1 = "Joke's End West Of First Boiler Room Block 1"
JokesEndWestOfFirstBoilerRoomBlock2 = "Joke's End West Of First Boiler Room Block 2"
JokesEndFirstBoilerRoomDigspot1 = "Joke's End First Boiler Room Digspot 1"
JokesEndFirstBoilerRoomDigspot2 = "Joke's End First Boiler Room Digspot 2"
JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1"
JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2"
JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3"
JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast Of Boiler Room 1 Block"
JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast Of Boiler Room 3 Digspot"
JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast Of Boiler Room 2 Block"
JokesEndNortheastOfBoilerRoom2Block2 = "Joke's End Northeast Of Boiler Room 2 Digspot"
JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1"
JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2"
JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3"
JokesEndSecondFloorWestRoomBlock4 = "Joke's End Second Floor West Room Block 4"
JokesEndSecondFloorEastRoomDigspot = "Joke's End Second Floor East Room Digspot"
JokesEndFinalSplitUpRoomDigspot = "Joke's End Final Split Up Room Digspot"
JokesEndSouthOfBridgeRoomBlock = "Joke's End South Of Bridge Room Block"
JokesEndSoloLuigiRoom1Block = "Joke's End Solo Luigi Room 1 Block"
JokesEndSoloLuigiRoom1Digspot = "Joke's End Solo Luigi Room 1 Digspot"
JokesEndSoloMarioFinalRoomBlock1 = "Joke's End Solo Mario Final Room Block 1"
JokesEndSoloMarioFinalRoomBlock2 = "Joke's End Solo Mario Final Room Block 2"
JokesEndSoloMarioFinalRoomBlock3 = "Joke's End Solo Mario Final Room Block 3"
JokesEndSoloLuigiRoom2Digspot = "Joke's End Solo Luigi Room 2 Digspot"
JokesEndSoloMarioRoom1Digspot = "Joke's End Solo Mario Room 1 Digspot"
JokesEndSoloMarioRoom2Block1 = "Joke's End Solo Mario Room 2 Block 1"
JokesEndSoloMarioRoom2Block2 = "Joke's End Solo Mario Room 2 Block 2"
JokesEndSoloMarioRoom2Block3 = "Joke's End Solo Mario Room 2 Block 3"
JokesEndSecondBoilerRoomDigspot1 = "Joke's End Second Boiler Room Digspot 1"
JokesEndSecondBoilerRoomDigspot2 = "Joke's End Second Boiler Room Digspot 2"
JokesEndNorthOfSecondBoilerRoomBlock1 = "Joke's End North Of Second Boiler Room Block 1"
JokesEndNorthOfSecondBoilerRoomBlock2 = "Joke's End North Of Second Boiler Room Block 2"
WinkleAreaColloseumDigspot = "Winkle Area Colloseum Digspot"
HoohooMountainFountainRoom2Block = "Hoohoo Mountain Fountain Room 2 Block"
HoohooMountainFountainRoom2Digspot = "Hoohoo Mountain Fountain Room 2 Digspot"
HoohooMountainPastHoohoorosConnectorRoomDigspot1 = "Hoohoo Mountain Past Hoohooros Connector Room Digspot 1"
HoohooMountainPastHoohoorosConnectorRoomBlock = "Hoohoo Mountain Past Hoohooros Connector Room Block"
HoohooMountainPastHoohoorosConnectorRoomDigspot2 = "Hoohoo Mountain Past Hoohooros Connector Room Digspot 2"
JokesEndBeforeJojoraRoomBlock1 = "Joke's End Before Jojora Room Block 1"
JokesEndBeforeJojoraRoomBlock2 = "Joke's End Before Jojora Room Block 2"
JokesEndBeforeJojoraRoomDigspot = "Joke's End Before Jojora Room Digspot"
JokesEndJojoraRoomDigspot = "Joke's End Jojora Room Digspot"
BeanbeanOutskirtsBeforeHarhallDigspot1 = "Beanbean Outskirts Before Harhall Digspot 1"
BeanbeanOutskirtsBeforeHarhallDigspot2 = "Beanbean Outskirts Before Harhall Digspot 2"
BeanbeanOutskirtsBroochGuardsRoomDigspot1 = "Beanbean Outskirts Brooch Guards Room Digspot 1"
BeanbeanOutskirtsBroochGuardsRoomDigspot2 = "Beanbean Outskirts Brooch Guards Room Digspot 2"
BeanbeanOutskirtsChateauEntranceDigspot1 = "Beanbean Outskirts Chateau Entrance Digspot 1"
BeanbeanOutskirtsChateauEntranceDigspot2 = "Beanbean Outskirts Chateau Entrance Digspot 2"
BeanbeanOutskirtsSouthOfHooniversityGuardsDigspot1 = "Beanbean Outskirts South of Hooniversity Guards Digspot 1"
BeanbeanOutskirtsSouthOfHooniversityGuardsDigspot2 = "Beanbean Outskirts South of Hooniversity Guards Digspot 2"
BeanbeanOutskirtsSouthOfHooniversityGuardsDigspot3 = "Beanbean Outskirts South of Hooniversity Guards Digspot 3"
OutsideWoohooHooniversityBlock = "Outside Woohoo Hooniversity Block"
BeanbeanOutskirtsEntranceToHoohooMountainBaseDigspot1 = (
"Beanbean Outskirts Entrance to Hoohoo Mountain Base Digspot 1"
)
BeanbeanOutskirtsEntranceToHoohooMountainBaseDigspot2 = (
"Beanbean Outskirts Entrance to Hoohoo Mountain Base Digspot 2"
)
WoohooHooniversitySoloMarioBarrelAreaBlock1 = "Woohoo Hooniversity Solo Mario Barrel Area Block 1"
WoohooHooniversitySoloMarioBarrelAreaBlock2 = "Woohoo Hooniversity Solo Mario Barrel Area Block 2"
WoohooHooniversitySoloMarioBarrelAreaBlock3 = "Woohoo Hooniversity Solo Mario Barrel Area Block 3"
BeanbeanOutskirtsPipe2RoomDigspot = "Beanbean Outskirts Pipe 2 Room Digspot"
BeanbeanOutskirtsPipe4RoomDigspot = "Beanbean Outskirts Pipe 4 Room Digspot"
BeanbeanCastleTownBeanletReward = "Beanbean Castle Town Beanlet Reward"
HoohooVillageMoleBehindTurtle = "Hoohoo Village Mole Behind Turtle"
HoohooMountainBaseMoleNearTeeheeValley = "Hoohoo Mountain Base Mole Near Teehee Valley"
BeanbeanOutskirtsSoloLuigiCaveMole = "Beanbean Outskirts Solo Luigi Cave Mole"
BeanbeanOutskirtsFarmRoomMoleReward1 = "Beanbean Outskirts Farm Room Mole Reward 1"
BeanbeanOutskirtsFarmRoomMoleReward2 = "Beanbean Outskirts Farm Room Mole Reward 2"
JokesEndMoleReward1 = "Joke's End Mole Reward 1"
JokesEndMoleReward2 = "Joke's End Mole Reward 2"
NorthOceanWhirlpoolMole = "North Ocean Whirlpool Mole"
BeanbeanOutskirtsNESoloMarioMole1 = "Beanbean Outskirts NE Solo Mario Mole 1"
HoohooVillageHammers = "Hoohoo Village Hammers"
BeanbeanOutskirtsSuperHammerUpgrade = "Beanbean Outskirts Super Hammer Upgrade"
BeanbeanOutskirtsUltraHammerUpgrade = "Beanbean Outskirts Ultra Hammer Upgrade"
OhoOasisFirebrand = "Oho Oasis Firebrand"
OhoOasisThunderhand = "Oho Oasis Thunderhand"
ChucklehuckWoodsRedChuckolaFruit = "Chucklehuck Woods Red Chuckola Fruit"
ChucklehuckWoodsWhiteChuckolaFruit = "Chucklehuck Woods White Chuckola Fruit"
ChucklehuckWoodsPurpleChuckolaFruit = "Chucklehuck Woods Purple Chuckola Fruit"
SSChuckolaMembershipCard = "S.S. Chuckola Membership Card"
WinkleAreaWinkleCard = "Winkle Area Winkle Card"
BeanbeanCastlePeachsExtraDress = "Beanbean Castle Peach's Extra Dress"
BeanbeanCastleFakeBeastar = "Beanbean Castle Fake Beanstar"
BeanbeanCastleTownBeanlet1 = "Beanbean Castle Town Beanlet 1"
BeanbeanCastleTownBeanlet2 = "Beanbean Castle Town Beanlet 2"
BeanbeanCastleTownBeanlet3 = "Beanbean Castle Town Beanlet 3"
BeanbeanCastleTownBeanlet4 = "Beanbean Castle Town Beanlet 4"
BeanbeanCastleTownBeanlet5 = "Beanbean Castle Town Beanlet 5"
BeanbeanCastleTownBeanstone1 = "Beanbean Castle Town Beanstone 1"
BeanbeanCastleTownBeanstone2 = "Beanbean Castle Town Beanstone 2"
BeanbeanCastleTownBeanstone3 = "Beanbean Castle Town Beanstone 3"
BeanbeanCastleTownBeanstone4 = "Beanbean Castle Town Beanstone 4"
BeanbeanCastleTownBeanstone5 = "Beanbean Castle Town Beanstone 5"
BeanbeanCastleTownBeanstone6 = "Beanbean Castle Town Beanstone 6"
BeanbeanCastleTownBeanstone7 = "Beanbean Castle Town Beanstone 7"
BeanbeanCastleTownBeanstone8 = "Beanbean Castle Town Beanstone 8"
BeanbeanCastleTownBeanstone9 = "Beanbean Castle Town Beanstone 9"
BeanbeanCastleTownBeanstone10 = "Beanbean Castle Town Beanstone 10"
YoshiTheaterBlueYoshi = "Yoshi Theater Blue Yoshi"
YoshiTheaterRedYoshi = "Yoshi Theater Red Yoshi"
YoshiTheaterGreenYoshi = "Yoshi Theater Green Yoshi"
YoshiTheaterYellowYoshi = "Yoshi Theater Yellow Yoshi"
YoshiTheaterPurpleYoshi = "Yoshi Theater Purple Yoshi"
YoshiTheaterOrangeYoshi = "Yoshi Theater Orange Yoshi"
YoshiTheaterAzureYoshi = "Yoshi Theater Azure Yoshi"
BeanbeanCastleBeanbeanBrooch = "Beanbean Castle Beanbean Brooch"
BeanbeanOutskirtsSecretScroll1 = "Beanbean Outskirts Secret Scroll 1"
BeanbeanOutskirtsSecretScroll2 = "Beanbean Outskirts Secret Scroll 2"
BeanbeanOutskirtsBeanFruit1 = "Beanbean Outskirts Bean Fruit 1"
BeanbeanOutskirtsBeanFruit2 = "Beanbean Outskirts Bean Fruit 2"
BeanbeanOutskirtsBeanFruit3 = "Beanbean Outskirts Bean Fruit 3"
BeanbeanOutskirtsBeanFruit4 = "Beanbean Outskirts Bean Fruit 4"
BeanbeanOutskirtsBeanFruit5 = "Beanbean Outskirts Bean Fruit 5"
BeanbeanOutskirtsBeanFruit6 = "Beanbean Outskirts Bean Fruit 6"
BeanbeanOutskirtsBeanFruit7 = "Beanbean Outskirts Bean Fruit 7"
HoohooMountainPeasleysRose = "Hoohoo Mountain Peasley's Rose"
ChateauGreenGoblet = "Chateau Green Goblet"
ChateauRedGoblet = "Chateau Red Goblet"
GwarharLagoonRedPearlBean = "Gwarhar Lagoon Red Pearl Bean"
GwarharLagoonGreenPearlBean = "Gwarhar Lagoon Green Pearl Bean"
GwarharLagoonSpangle = "Gwarhar Lagoon Spangle"
BeanstarPieceWinkleArea = "Beanstar Piece Winkle Area"
BeanstarPieceHarhall = "Beanstar Piece Harhall"
BeanstarPieceYoshiTheater = "Beanstar Piece Yoshi Theater"
BeanstarPieceHermie = "Beanstar Piece Hermie"
ShopStartingFlag1 = "Shop Starting Flag 1"
ShopStartingFlag2 = "Shop Starting Flag 2"
ShopStartingFlag3 = "Shop Starting Flag 3"
ShopChuckolatorFlag = "Shop Chuckolator Flag"
ShopMomPiranhaFlag1 = "Shop Mom Piranha Flag 1"
ShopMomPiranhaFlag2 = "Shop Mom Piranha Flag 2"
ShopMomPiranhaFlag3 = "Shop Mom Piranha Flag 3"
ShopMomPiranhaFlag4 = "Shop Mom Piranha Flag 4"
ShopPeachKidnappedFlag1 = "Shop Enter Fungitown Flag 1"
ShopPeachKidnappedFlag2 = "Shop Enter Fungitown Flag 2"
FungitownShopStartingFlag1 = "Fungitown Shop Starting Flag 1"
FungitownShopStartingFlag2 = "Fungitown Shop Starting Flag 2"
FungitownShopStartingFlag3 = "Fungitown Shop Starting Flag 3"
FungitownShopStartingFlag4 = "Fungitown Shop Starting Flag 4"
FungitownShopStartingFlag5 = "Fungitown Shop Starting Flag 5"
FungitownShopStartingFlag6 = "Fungitown Shop Starting Flag 6"
FungitownShopStartingFlag7 = "Fungitown Shop Starting Flag 7"
FungitownShopStartingFlag8 = "Fungitown Shop Starting Flag 8"
ShopBeanstarCompleteFlag1 = "Shop Beanstar Complete Flag 1"
ShopBeanstarCompleteFlag2 = "Shop Beanstar Complete Flag 2"
ShopBeanstarCompleteFlag3 = "Shop Beanstar Complete Flag 3"
FungitownShopBeanstarCompleteFlag = "Fungitown Shop Beanstar Complete Flag"
ShopBirdoFlag = "Shop Birdo Flag"
FungitownShopBirdoFlag = "Fungitown Shop Birdo Flag"
CoffeeShopBrewReward1 = "Coffee Shop Brew Reward 1"
CoffeeShopBrewReward2 = "Coffee Shop Brew Reward 2"
CoffeeShopBrewReward3 = "Coffee Shop Brew Reward 3"
CoffeeShopBrewReward4 = "Coffee Shop Brew Reward 4"
CoffeeShopBrewReward5 = "Coffee Shop Brew Reward 5"
CoffeeShopBrewReward6 = "Coffee Shop Brew Reward 6"
CoffeeShopBrewReward7 = "Coffee Shop Brew Reward 7"
CoffeeShopWoohooBlend = "Coffee Shop Woohoo Blend"
CoffeeShopHoohooBlend = "Coffee Shop Hoohoo Blend"
CoffeeShopChuckleBlend = "Coffee Shop Chuckle Blend"
CoffeeShopTeeheeBlend = "Coffee Shop Teehee Blend"
CoffeeShopHoolumbian = "Coffee Shop Hoolumbian"
CoffeeShopChuckoccino = "Coffee Shop Chuckoccino"
CoffeeShopTeeheespresso = "Coffee Shop Teeheespresso"
PantsShopStartingFlag1 = "Pants Shop Starting Flag 1"
PantsShopStartingFlag2 = "Pants Shop Starting Flag 2"
PantsShopStartingFlag3 = "Pants Shop Starting Flag 3"
PantsShopChuckolatorFlag1 = "Pants Shop Chuckolator Flag 1"
PantsShopChuckolatorFlag2 = "Pants Shop Chuckolator Flag 2"
PantsShopChuckolatorFlag3 = "Pants Shop Chuckolator Flag 3"
PantsShopMomPiranhaFlag1 = "Pants Shop Mom Piranha Flag 1"
PantsShopMomPiranhaFlag2 = "Pants Shop Mom Piranha Flag 2"
PantsShopMomPiranhaFlag3 = "Pants Shop Mom Piranha Flag 3"
PantsShopPeachKidnappedFlag1 = "Pants Shop Enter Fungitown Flag 1"
PantsShopPeachKidnappedFlag2 = "Pants Shop Enter Fungitown Flag 2"
PantsShopPeachKidnappedFlag3 = "Pants Shop Enter Fungitown Flag 3"
PantsShopBeanstarCompleteFlag1 = "Pants Shop Beanstar Complete Flag 1"
PantsShopBeanstarCompleteFlag2 = "Pants Shop Beanstar Complete Flag 2"
PantsShopBeanstarCompleteFlag3 = "Pants Shop Beanstar Complete Flag 3"
PantsShopBirdoFlag1 = "Pants Shop Birdo Flag 1"
PantsShopBirdoFlag2 = "Pants Shop Birdo Flag 2"
PantsShopBirdoFlag3 = "Pants Shop Birdo Flag 3"
FungitownPantsShopStartingFlag1 = "Fungitown Pants Shop Starting Flag 1"
FungitownPantsShopStartingFlag2 = "Fungitown Pants Shop Starting Flag 2"
FungitownPantsShopStartingFlag3 = "Fungitown Pants Shop Starting Flag 3"
FungitownPantsShopBeanstarCompleteFlag1 = "Fungitown Pants Shop Beanstar Complete Flag 1"
FungitownPantsShopBeanstarCompleteFlag2 = "Fungitown Pants Shop Beanstar Complete Flag 2"
FungitownPantsShopBirdoFlag1 = "Fungitown Pants Shop Birdo Flag 1"
FungitownPantsShopBirdoFlag2 = "Fungitown Pants Shop Birdo Flag 2"
BeanbeanOutskirtsNESoloMarioMole2 = "Beanbean Outskirts NE Solo Mario Mole 2"
GwarharLagoonSpangleReward = "Gwarhar Lagoon Spangle Reward"
BowsersCastleEntranceBlock1 = "Bowser's Castle Entrance Block 1"
BowsersCastleEntranceBlock2 = "Bowser's Castle Entrance Block 2"
BowsersCastleEntranceDigspot = "Bowser's Castle Entrance Digspot"
BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1"
BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2"
BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot"
BowsersCastleAfterMortonBlock = "Bowser's Castle After Morton Block"
BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1"
BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2"
BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1"
BowsersCastleRoyCorridorBlock2 = "Bowser's Castle Roy Corridor Block 2"
BowsersCastleWendyLarryHallwayDigspot = "Bowser's Castle Wendy & Larry Hallway Digspot"
BowsersCastleBeforeFawfulFightBlock1 = "Bowser's Castle Before Fawful Fight Block 1"
BowsersCastleBeforeFawfulFightBlock2 = "Bowser's Castle Before Fawful Fight Block 2"
BowsersCastleGreatDoorBlock1 = "Bowser's Castle Great Door Block 1"
BowsersCastleGreatDoorBlock2 = "Bowser's Castle Great Door Block 2"
BowsersCastleMortonRoom1Digspot = "Bowser's Castle Morton Room 1 Digspot"
BowsersCastleLemmyRoom1Block = "Bowser's Castle Lemmy Room 1 Block"
BowsersCastleLemmyRoom1Digspot = "Bowser's Castle Lemmy Room 1 Digspot"
BowsersCastleLudwigRoom1Block = "Bowser's Castle Ludwig Room 1 Block"
BowsersCastleMiniMarioSidescrollerBlock1 = "Bowser's Castle Mini Mario Sidescroller Block 1"
BowsersCastleMiniMarioSidescrollerBlock2 = "Bowser's Castle Mini Mario Sidescroller Block 2"
BowsersCastleMiniMarioMazeBlock1 = "Bowser's Castle Mini Mario Maze Block 1"
BowsersCastleMiniMarioMazeBlock2 = "Bowser's Castle Mini Mario Maze Block 2"
BowsersCastleBeforeWendyFightBlock1 = "Bowser's Castle Before Wendy Fight Block 1"
BowsersCastleBeforeWendyFightBlock2 = "Bowser's Castle Before Wendy Fight Block 2"
BowsersCastleLarryRoomBlock = "Bowser's Castle Larry Room Block"
BowsersCastleLemmyRoomMole = "Bowser's Castle Lemmy Room Mole"
SurfMinigame = "Surf Minigame"
BeanbeanOutskirtsThunderHandMole = "Beanbean Outskirts Thunderhand Mole"
BadgeShopMomPiranhaFlag1 = "Badge Shop Mom Piranha Flag 1"
BadgeShopMomPiranhaFlag2 = "Badge Shop Mom Piranha Flag 2"
BadgeShopMomPiranhaFlag3 = "Badge Shop Mom Piranha Flag 3"
HarhallsPants = "Harhall's Pants"
HoohooMountainBaseBooStatueCaveCoinBlock1 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 1"
HoohooMountainBaseBooStatueCaveCoinBlock2 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 2"
HoohooMountainBaseBooStatueCaveCoinBlock3 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 3"
BeanbeanOutskirtsNWCoinBlock = "Beanbean Outskirts NW Coin Block"
BeanbeanOutskirtsSRoom1CoinBlock = "Beanbean Outskirts S Room 1 Coin Block"
BeanbeanOutskirtsSRoom2CoinBlock = "Beanbean Outskirts S Room 2 Coin Block"
ChateauPoppleRoomCoinBlock1 = "Chateau Popple Room Coin Block 1"
ChateauPoppleRoomCoinBlock2 = "Chateau Popple Room Coin Block 2"
ChucklehuckWoodsCaveRoom1CoinBlock = "Chucklehuck Woods Cave Room 1 Coin Block"
ChucklehuckWoodsCaveRoom2CoinBlock = "Chucklehuck Woods Cave Room 2 Coin Block"
ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block"
ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block"
ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block"
ChucklehuckWoodsAfterChucklerootCoinBlock = "Chucklehuck Woods After Chuckleroot Coin Block"
ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block"
ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block"
SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block"
TeeheeValleyPastUltraHammerRocksCoinBlock = "Teehee Valley Past Ultra Hammer Rocks Coin Block"
SSChuckolaStorageRoomCoinBlock1 = "S.S. Chuckola Storage Room Coin Block 1"
SSChuckolaStorageRoomCoinBlock2 = "S.S. Chuckola Storage Room Coin Block 2"
GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock = "Gwarhar Lagoon First Underwater Area Room 2 Coin Block"
JokesEndSecondFloorWestRoomCoinBlock = "Joke's End Second Floor West Room Coin Block"
JokesEndNorthofBridgeRoomCoinBlock = "Joke's End North of Bridge Room Coin Block"

299
worlds/mlss/Options.py Normal file
View File

@@ -0,0 +1,299 @@
from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range
from dataclasses import dataclass
class BowsersCastleSkip(Toggle):
"""
Skip straight from the entrance hall to Bowletta in Bowser's Castle.
All Bowser's Castle locations will be removed from the location pool.
"""
display_name = "Bowser's Castle Skip"
class ExtraPipes(Toggle):
"""
Gives the player access to pipes 1, 3, 4, and 6 from the start.
"""
display_name = "Start With Extra Pipes"
class SkipMinecart(Toggle):
"""
Skip the minecart minigame that leads you through Hoohoo Mountain Base.
This will remove the 1 location in the minecart cave from the location pool.
"""
display_name = "Skip Minecart Minigame"
class DisableSurf(Toggle):
"""
Remove the surf minigame location from the location pool.
"""
display_name = "Disable Surf Minigame"
class MusicOptions(Choice):
"""
Choose if you want to randomize or disable music.
default: Music will be untouched.
randomize: Music will be randomized.
disable: All music will be disabled. No music will play throughout the entire game.
"""
display_name = "Music Options"
option_default = 0
option_randomize = 1
option_disable = 2
default = 0
class RandomSounds(Toggle):
"""
Randomizes every sound in the game, minus a select few that can softlock the game.
"""
display_name = "Randomize Sounds"
class MarioColor(Choice):
"""
This changes the color of Mario's hat, as well as some key colors that are red including UI etc.
"""
display_name = "Mario's Color"
option_red = 0
option_green = 1
option_blue = 2
option_cyan = 3
option_yellow = 4
option_orange = 5
option_purple = 6
option_pink = 7
option_black = 8
option_white = 9
option_silhouette = 10
option_chaos = 11
option_true_chaos = 12
default = 0
class LuigiColor(Choice):
"""
This changes the color of Luigi's hat, as well as some key colors that are green including UI etc.
"""
display_name = "Luigi's Color"
option_red = 0
option_green = 1
option_blue = 2
option_cyan = 3
option_yellow = 4
option_orange = 5
option_purple = 6
option_pink = 7
option_black = 8
option_white = 9
option_silhouette = 10
option_chaos = 11
option_true_chaos = 12
default = 1
class MarioPants(Choice):
"""
This changes the color of Mario's trousers.
"""
display_name = "Mario's Pants Color"
option_vanilla = 0
option_red = 1
option_green = 2
option_blue = 3
option_cyan = 4
option_yellow = 5
option_orange = 6
option_purple = 7
option_pink = 8
option_black = 9
option_white = 10
option_chaos = 11
default = 0
class LuigiPants(Choice):
"""
This changes the color of Luigi's trousers.
"""
display_name = "Luigi's Pants Color"
option_vanilla = 0
option_red = 1
option_green = 2
option_blue = 3
option_cyan = 4
option_yellow = 5
option_orange = 6
option_purple = 7
option_pink = 8
option_black = 9
option_white = 10
option_chaos = 11
default = 0
class RandomizeEnemies(Choice):
"""
Randomize all normal enemy encounters in the game.
If Bowser's castle skip is enabled, then enemies from Bowser's Castle will not be included.
Disabled: Enemies will not be randomized.
Vanilla Groups: Vanilla enemy groups will be shuffled with each other. Custom enemy groups will not be made.
Custom Groups: Custom enemy groups will be made and shuffled. Some enemy groups will only be semi-random,
including groups with flying enemies or pestnuts in them.
"""
display_name = "Randomize Enemies"
option_disabled = 0
option_vanilla_groups = 1
option_custom_groups = 2
default = 0
class RandomizeBosses(Choice):
"""
Randomize all boss encounters in the game.
If Bowser's castle skip is enabled then bosses from Bowser's Castle will not be included.
Some bosses are not randomized due to flags, and story (such as the final boss).
Boss Only: Bosses will only be swapped with another boss.
Boss Normal: Bosses can be swapped with normal enemy encounters.
"""
display_name = "Randomize Bosses"
option_disabled = 0
option_boss_only = 1
option_boss_normal = 2
default = 0
class ScaleStats(Toggle):
"""
This scales enemy HP, POW, DEF, and XP to vanilla values.
This setting is intended for use with the Enemy Randomizer and is Recommended to turn on.
If you are not using the Enemy Randomizer the effects will be minimal.
"""
display_name = "Scale Enemy Stats"
class XPMultiplier(Range):
"""
This will multiply any XP you receive in battle by the chosen multiplier.
"""
display_name = "XP Multiplier"
range_start = 0
range_end = 4
default = 1
class TattleHp(Toggle):
"""
This will display the enemies' current and max health while in battle.
"""
display_name = "Tattle HP"
class RandomizeBackgrounds(Toggle):
"""
This randomizes the background image in battles.
"""
display_name = "Randomize Battle Backgrounds"
class HiddenVisible(Choice):
"""
This makes any hidden blocks in the game into regular item blocks and vice versa.
Disabled: Hidden blocks will remain invisible.
Hidden Visible: Hidden blocks will turn visible to the player.
Blocks Invisible: All item blocks will turn invisible. Hidden blocks will also remain invisible.
"""
display_name = "Item Block Visibility"
option_disabled = 0
option_hidden_visible = 1
option_blocks_invisible = 2
default = 0
class Coins(Toggle):
"""
Add all coin blocks in the game to the location pool.
"""
display_name = "Coin Blocks"
class HarhallsPants(Toggle):
"""
This will remove the Harhall's Pants check from the pool.
"""
display_name = "Remove Harhall's Pants"
class DifficultLogic(Toggle):
"""
This adjusts the logic to be more difficult in a few areas,
allowing for the logic to account for players getting to certain areas in unintended ways.
Enable at your own risk, this is not an option made for beginners.
"""
display_name = "Difficult Logic"
class ChuckleBeans(Choice):
"""
Choose how you want chuckle bean digspots to be randomized.
An amount of chuckle beans will be removed from the item pool,
equal to the amount of locations removed by the setting that you choose.
None: No chuckle bean digspots will be added into the location pool.
Only Visible: Only chuckle bean digspots clearly marked with an X will be added into the location pool.
All: All chuckle bean digspots will be added into the location pool.
"""
display_name = "Chuckle Beans"
option_none = 0
option_only_visible = 1
option_all = 2
default = 2
@dataclass
class MLSSOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
coins: Coins
difficult_logic: DifficultLogic
castle_skip: BowsersCastleSkip
extra_pipes: ExtraPipes
skip_minecart: SkipMinecart
disable_surf: DisableSurf
harhalls_pants: HarhallsPants
block_visibility: HiddenVisible
chuckle_beans: ChuckleBeans
music_options: MusicOptions
randomize_sounds: RandomSounds
randomize_enemies: RandomizeEnemies
randomize_bosses: RandomizeBosses
randomize_backgrounds: RandomizeBackgrounds
scale_stats: ScaleStats
xp_multiplier: XPMultiplier
tattle_hp: TattleHp
mario_color: MarioColor
luigi_color: LuigiColor
mario_pants: MarioPants
luigi_pants: LuigiPants

323
worlds/mlss/Regions.py Normal file
View File

@@ -0,0 +1,323 @@
import typing
from BaseClasses import Region, Entrance
from .Locations import (
MLSSLocation,
mainArea,
chucklehuck,
castleTown,
startingFlag,
chuckolatorFlag,
piranhaFlag,
kidnappedFlag,
beanstarFlag,
birdoFlag,
surfable,
hooniversity,
gwarharEntrance,
gwarharMain,
fungitown,
fungitownBeanstar,
fungitownBirdo,
teeheeValley,
winkle,
sewers,
airport,
bowsers,
bowsersMini,
jokesEntrance,
jokesMain,
theater,
booStatue,
oasis,
postJokes,
baseUltraRocks,
coins,
)
from . import StateLogic
if typing.TYPE_CHECKING:
from . import MLSSWorld
def create_regions(world: "MLSSWorld", excluded: typing.List[str]):
menu_region = Region("Menu", world.player, world.multiworld)
world.multiworld.regions.append(menu_region)
create_region(world, "Main Area", mainArea, excluded)
create_region(world, "Chucklehuck Woods", chucklehuck, excluded)
create_region(world, "Beanbean Castle Town", castleTown, excluded)
create_region(world, "Shop Starting Flag", startingFlag, excluded)
create_region(world, "Shop Chuckolator Flag", chuckolatorFlag, excluded)
create_region(world, "Shop Mom Piranha Flag", piranhaFlag, excluded)
create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag, excluded)
create_region(world, "Shop Beanstar Complete Flag", beanstarFlag, excluded)
create_region(world, "Shop Birdo Flag", birdoFlag, excluded)
create_region(world, "Surfable", surfable, excluded)
create_region(world, "Hooniversity", hooniversity, excluded)
create_region(world, "GwarharEntrance", gwarharEntrance, excluded)
create_region(world, "GwarharMain", gwarharMain, excluded)
create_region(world, "TeeheeValley", teeheeValley, excluded)
create_region(world, "Winkle", winkle, excluded)
create_region(world, "Sewers", sewers, excluded)
create_region(world, "Airport", airport, excluded)
create_region(world, "JokesEntrance", jokesEntrance, excluded)
create_region(world, "JokesMain", jokesMain, excluded)
create_region(world, "PostJokes", postJokes, excluded)
create_region(world, "Theater", theater, excluded)
create_region(world, "Fungitown", fungitown, excluded)
create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar, excluded)
create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo, excluded)
create_region(world, "BooStatue", booStatue, excluded)
create_region(world, "Oasis", oasis, excluded)
create_region(world, "BaseUltraRocks", baseUltraRocks, excluded)
if world.options.coins:
create_region(world, "Coins", coins, excluded)
if not world.options.castle_skip:
create_region(world, "Bowser's Castle", bowsers, excluded)
create_region(world, "Bowser's Castle Mini", bowsersMini, excluded)
def connect_regions(world: "MLSSWorld"):
names: typing.Dict[str, int] = {}
connect(world, names, "Menu", "Main Area")
if world.options.coins:
connect(world, names, "Main Area", "Coins")
connect(world, names, "Main Area", "BaseUltraRocks", lambda state: StateLogic.ultra(state, world.player))
connect(world, names, "Main Area", "Chucklehuck Woods", lambda state: StateLogic.brooch(state, world.player))
connect(world, names, "Main Area", "BooStatue", lambda state: StateLogic.canCrash(state, world.player))
connect(
world,
names,
"Main Area",
"Hooniversity",
lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
)
connect(world, names, "Hooniversity", "Oasis")
connect(
world,
names,
"Main Area",
"TeeheeValley",
lambda state: StateLogic.super(state, world.player) or StateLogic.canDash(state, world.player),
)
connect(
world,
names,
"TeeheeValley",
"GwarharEntrance",
lambda state: StateLogic.membership(state, world.player) and StateLogic.fire(state, world.player),
)
connect(
world,
names,
"TeeheeValley",
"Oasis",
lambda state: StateLogic.membership(state, world.player) and StateLogic.fire(state, world.player),
)
connect(
world,
names,
"TeeheeValley",
"Fungitown",
lambda state: StateLogic.thunder(state, world.player)
and StateLogic.castleTown(state, world.player)
and StateLogic.rose(state, world.player),
)
connection = connect(
world,
names,
"Fungitown",
"Fungitown Shop Beanstar Complete Flag",
lambda state: StateLogic.pieces(state, world.player) or StateLogic.fungitown_birdo_shop(state, world.player),
True,
)
world.multiworld.register_indirect_condition(world.get_region("Fungitown Shop Birdo Flag"), connection)
connect(world, names, "Main Area", "Shop Starting Flag")
connection = connect(
world,
names,
"Shop Starting Flag",
"Shop Chuckolator Flag",
lambda state: (
StateLogic.brooch(state, world.player)
and StateLogic.fruits(state, world.player)
and (
StateLogic.thunder(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.hammers(state, world.player)
)
)
or (
StateLogic.piranha_shop(state, world.player)
or StateLogic.fungitown_shop(state, world.player)
or StateLogic.star_shop(state, world.player)
or StateLogic.birdo_shop(state, world.player)
),
True,
)
world.multiworld.register_indirect_condition(world.get_region("Shop Mom Piranha Flag"), connection)
world.multiworld.register_indirect_condition(world.get_region("Shop Enter Fungitown Flag"), connection)
world.multiworld.register_indirect_condition(world.get_region("Shop Beanstar Complete Flag"), connection)
world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
connection = connect(
world,
names,
"Shop Starting Flag",
"Shop Mom Piranha Flag",
lambda state: StateLogic.thunder(state, world.player)
or (
StateLogic.fungitown_shop(state, world.player)
or StateLogic.star_shop(state, world.player)
or StateLogic.birdo_shop(state, world.player)
),
True,
)
world.multiworld.register_indirect_condition(world.get_region("Shop Enter Fungitown Flag"), connection)
world.multiworld.register_indirect_condition(world.get_region("Shop Beanstar Complete Flag"), connection)
world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
connection = connect(
world,
names,
"Shop Starting Flag",
"Shop Enter Fungitown Flag",
lambda state: StateLogic.fungitown(state, world.player)
or (StateLogic.star_shop(state, world.player) or StateLogic.birdo_shop(state, world.player)),
True,
)
world.multiworld.register_indirect_condition(world.get_region("Shop Beanstar Complete Flag"), connection)
world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
connection = connect(
world,
names,
"Shop Starting Flag",
"Shop Beanstar Complete Flag",
lambda state: (
StateLogic.castleTown(state, world.player)
and StateLogic.pieces(state, world.player)
and StateLogic.rose(state, world.player)
)
or StateLogic.birdo_shop(state, world.player),
True,
)
world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
connect(world, names, "Main Area", "Sewers", lambda state: StateLogic.rose(state, world.player))
connect(world, names, "Main Area", "Airport", lambda state: StateLogic.thunder(state, world.player))
connect(world, names, "Main Area", "Theater", lambda state: StateLogic.canDash(state, world.player))
connect(world, names, "Main Area", "Surfable", lambda state: StateLogic.surfable(state, world.player))
connect(world, names, "Surfable", "GwarharEntrance")
connect(world, names, "Surfable", "Oasis")
connect(world, names, "Surfable", "JokesEntrance", lambda state: StateLogic.fire(state, world.player))
connect(world, names, "JokesMain", "PostJokes", lambda state: StateLogic.postJokes(state, world.player))
if not world.options.castle_skip:
connect(world, names, "PostJokes", "Bowser's Castle")
connect(
world,
names,
"Bowser's Castle",
"Bowser's Castle Mini",
lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player),
)
connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player))
connect(
world,
names,
"Chucklehuck Woods",
"Beanbean Castle Town",
lambda state: StateLogic.fruits(state, world.player)
and (
StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player)
),
)
if world.options.difficult_logic:
connect(world, names, "GwarharEntrance", "GwarharMain", lambda state: StateLogic.canDash(state, world.player))
connect(world, names, "JokesEntrance", "JokesMain", lambda state: StateLogic.canDig(state, world.player))
connect(
world,
names,
"Shop Starting Flag",
"Shop Birdo Flag",
lambda state: StateLogic.postJokes(state, world.player),
)
connect(
world,
names,
"Fungitown",
"Fungitown Shop Birdo Flag",
lambda state: StateLogic.postJokes(state, world.player),
)
else:
connect(
world,
names,
"GwarharEntrance",
"GwarharMain",
lambda state: StateLogic.canDash(state, world.player) and StateLogic.canCrash(state, world.player),
)
connect(
world,
names,
"JokesEntrance",
"JokesMain",
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.canDig(state, world.player),
)
connect(
world,
names,
"Shop Starting Flag",
"Shop Birdo Flag",
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player),
)
connect(
world,
names,
"Fungitown",
"Fungitown Shop Birdo Flag",
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player),
)
def create_region(world: "MLSSWorld", name, locations, excluded):
ret = Region(name, world.player, world.multiworld)
for location in locations:
loc = MLSSLocation(world.player, location.name, location.id, ret)
if location.name in excluded:
continue
ret.locations.append(loc)
world.multiworld.regions.append(ret)
def connect(
world: "MLSSWorld",
used_names: typing.Dict[str, int],
source: str,
target: str,
rule: typing.Optional[typing.Callable] = None,
reach: typing.Optional[bool] = False,
) -> Entrance | None:
source_region = world.multiworld.get_region(source, world.player)
target_region = world.multiworld.get_region(target, world.player)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (" " * used_names[target])
connection = Entrance(world.player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
if reach:
return connection
else:
return None

437
worlds/mlss/Rom.py Normal file
View File

@@ -0,0 +1,437 @@
import io
import json
import random
from . import Data
from typing import TYPE_CHECKING, Optional
from BaseClasses import Item, Location
from settings import get_settings
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
from .Items import item_table
from .Locations import shop, badge, pants, location_table, hidden, all_locations
if TYPE_CHECKING:
from . import MLSSWorld
colors = [
Data.redHat,
Data.greenHat,
Data.blueHat,
Data.azureHat,
Data.yellowHat,
Data.orangeHat,
Data.purpleHat,
Data.pinkHat,
Data.blackHat,
Data.whiteHat,
Data.silhouetteHat,
Data.chaosHat,
Data.truechaosHat
]
cpants = [
Data.vanilla,
Data.redPants,
Data.greenPants,
Data.bluePants,
Data.azurePants,
Data.yellowPants,
Data.orangePants,
Data.purplePants,
Data.pinkPants,
Data.blackPants,
Data.whitePants,
Data.chaosPants
]
def get_base_rom_as_bytes() -> bytes:
with open(get_settings().mlss_options.rom_file, "rb") as infile:
base_rom_bytes = bytes(infile.read())
return base_rom_bytes
class MLSSPatchExtension(APPatchExtension):
game = "Mario & Luigi Superstar Saga"
@staticmethod
def randomize_music(caller: APProcedurePatch, rom: bytes):
options = json.loads(caller.get_file("options.json").decode("UTF-8"))
if options["music_options"] != 1:
return rom
stream = io.BytesIO(rom)
random.seed(options["seed"] + options["player"])
songs = []
stream.seek(0x21CB74)
for _ in range(50):
if stream.tell() == 0x21CBD8:
stream.seek(4, 1)
continue
temp = stream.read(4)
songs.append(temp)
random.shuffle(songs)
stream.seek(0x21CB74)
for _ in range(50):
if stream.tell() == 0x21CBD8:
stream.seek(4, 1)
continue
stream.write(songs.pop())
return stream.getvalue()
@staticmethod
def hidden_visible(caller: APProcedurePatch, rom: bytes):
options = json.loads(caller.get_file("options.json").decode("UTF-8"))
if options["block_visibility"] == 0:
return rom
stream = io.BytesIO(rom)
for location in all_locations:
stream.seek(location.id - 6)
b = stream.read(1)
if b[0] == 0x10 and options["block_visibility"] == 1:
stream.seek(location.id - 6)
stream.write(bytes([0x0]))
if b[0] == 0x0 and options["block_visibility"] == 2:
stream.seek(location.id - 6)
stream.write(bytes([0x10]))
return stream.getvalue()
@staticmethod
def randomize_sounds(caller: APProcedurePatch, rom: bytes):
options = json.loads(caller.get_file("options.json").decode("UTF-8"))
if options["randomize_sounds"] != 1:
return rom
stream = io.BytesIO(rom)
random.seed(options["seed"] + options["player"])
fresh_pointers = Data.sounds
pointers = Data.sounds
random.shuffle(pointers)
stream.seek(0x21CC44, 0)
for i in range(354):
current_position = stream.tell()
value = int.from_bytes(stream.read(3), "little")
if value in fresh_pointers:
stream.seek(current_position)
stream.write(pointers.pop().to_bytes(3, "little"))
stream.seek(1, 1)
return stream.getvalue()
@staticmethod
def enemy_randomize(caller: APProcedurePatch, rom: bytes):
options = json.loads(caller.get_file("options.json").decode("UTF-8"))
if options["randomize_bosses"] == 0 and options["randomize_enemies"] == 0:
return rom
enemies = [pos for pos in Data.enemies if pos not in Data.bowsers] if options["castle_skip"] else Data.enemies
bosses = [pos for pos in Data.bosses if pos not in Data.bowsers] if options["castle_skip"] else Data.bosses
stream = io.BytesIO(rom)
random.seed(options["seed"] + options["player"])
if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2) and options["randomize_enemies"] == 0:
raw = []
for pos in bosses:
stream.seek(pos + 1)
raw += [stream.read(0x1F)]
random.shuffle(raw)
for pos in bosses:
stream.seek(pos + 1)
stream.write(raw.pop())
if options["randomize_enemies"] == 1:
raw = []
for pos in enemies:
stream.seek(pos + 1)
raw += [stream.read(0x1F)]
if options["randomize_bosses"] == 2:
for pos in bosses:
stream.seek(pos + 1)
raw += [stream.read(0x1F)]
random.shuffle(raw)
for pos in enemies:
stream.seek(pos + 1)
stream.write(raw.pop())
if options["randomize_bosses"] == 2:
for pos in bosses:
stream.seek(pos + 1)
stream.write(raw.pop())
return stream.getvalue()
enemies_raw = []
groups = []
if options["randomize_enemies"] == 0:
return stream.getvalue()
if options["randomize_bosses"] == 2:
for pos in bosses:
stream.seek(pos + 1)
groups += [stream.read(0x1F)]
for pos in enemies:
stream.seek(pos + 8)
for _ in range(6):
enemy = int.from_bytes(stream.read(1))
if enemy > 0:
stream.seek(1, 1)
flag = int.from_bytes(stream.read(1))
if flag == 0x7:
break
if flag in [0x0, 0x2, 0x4]:
if enemy not in Data.pestnut and enemy not in Data.flying:
enemies_raw += [enemy]
stream.seek(1, 1)
else:
stream.seek(3, 1)
random.shuffle(enemies_raw)
chomp = False
for pos in enemies:
stream.seek(pos + 8)
for _ in range(6):
enemy = int.from_bytes(stream.read(1))
if enemy > 0 and enemy not in Data.flying and enemy not in Data.pestnut:
if enemy == 0x52:
chomp = True
stream.seek(1, 1)
flag = int.from_bytes(stream.read(1))
if flag not in [0x0, 0x2, 0x4]:
stream.seek(1, 1)
continue
stream.seek(-3, 1)
stream.write(bytes([enemies_raw.pop()]))
stream.seek(1, 1)
stream.write(bytes([0x6]))
stream.seek(1, 1)
else:
stream.seek(3, 1)
stream.seek(pos + 1)
raw = stream.read(0x1F)
if chomp:
raw = raw[0:3] + bytes([0x67, 0xAB, 0x28, 0x08]) + raw[7:]
else:
raw = raw[0:3] + bytes([0xEE, 0x2C, 0x28, 0x08]) + raw[7:]
groups += [raw]
chomp = False
random.shuffle(groups)
arr = enemies
if options["randomize_bosses"] == 2:
arr += bosses
for pos in arr:
stream.seek(pos + 1)
stream.write(groups.pop())
return stream.getvalue()
class MLSSProcedurePatch(APProcedurePatch, APTokenMixin):
game = "Mario & Luigi Superstar Saga"
hash = "4b1a5897d89d9e74ec7f630eefdfd435"
patch_file_ending = ".apmlss"
result_file_ending = ".gba"
procedure = [
("apply_bsdiff4", ["base_patch.bsdiff4"]),
("apply_tokens", ["token_data.bin"]),
("enemy_randomize", []),
("hidden_visible", []),
("randomize_sounds", []),
("randomize_music", []),
]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_as_bytes()
def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
options_dict = {
"randomize_enemies": world.options.randomize_enemies.value,
"randomize_bosses": world.options.randomize_bosses.value,
"castle_skip": world.options.castle_skip.value,
"randomize_sounds": world.options.randomize_sounds.value,
"music_options": world.options.music_options.value,
"block_visibility": world.options.block_visibility.value,
"seed": world.multiworld.seed,
"player": world.player,
}
patch.write_file("options.json", json.dumps(options_dict).encode("UTF-8"))
# Bake player name into ROM
patch.write_token(APTokenTypes.WRITE, 0xDF0000, world.multiworld.player_name[world.player].encode("UTF-8"))
# Bake seed name into ROM
patch.write_token(APTokenTypes.WRITE, 0xDF00A0, world.multiworld.seed_name.encode("UTF-8"))
# Bake patch into header
patch.write_token(APTokenTypes.WRITE, 0xAD, "P".encode("UTF-8"))
# Intro Skip
patch.write_token(
APTokenTypes.WRITE,
0x244D08,
bytes([0x88, 0x0, 0x19, 0x91, 0x1, 0x20, 0x58, 0x1, 0xF, 0xA0, 0x3, 0x15, 0x27, 0x8]),
)
# Patch S.S Chuckola Loading Zones
patch.write_token(APTokenTypes.WRITE, 0x25FD4E, bytes([0x48, 0x30, 0x80, 0x60, 0x50, 0x2, 0xF]))
patch.write_token(APTokenTypes.WRITE, 0x25FD83, bytes([0x48, 0x30, 0x80, 0x60, 0xC0, 0x2, 0xF]))
patch.write_token(APTokenTypes.WRITE, 0x25FDB8, bytes([0x48, 0x30, 0x05, 0x80, 0xE4, 0x0, 0xF]))
patch.write_token(APTokenTypes.WRITE, 0x25FDED, bytes([0x48, 0x30, 0x06, 0x80, 0xE4, 0x0, 0xF]))
patch.write_token(APTokenTypes.WRITE, 0x25FE22, bytes([0x48, 0x30, 0x07, 0x80, 0xE4, 0x0, 0xF]))
patch.write_token(APTokenTypes.WRITE, 0x25FE57, bytes([0x48, 0x30, 0x08, 0x80, 0xE4, 0x0, 0xF]))
if world.options.extra_pipes:
patch.write_token(APTokenTypes.WRITE, 0xD00001, bytes([0x1]))
if world.options.castle_skip:
patch.write_token(APTokenTypes.WRITE, 0x3AEAB0, bytes([0xC1, 0x67, 0x0, 0x6, 0x1C, 0x08, 0x3]))
patch.write_token(APTokenTypes.WRITE, 0x3AEC18, bytes([0x89, 0x65, 0x0, 0xE, 0xA, 0x08, 0x1]))
if world.options.skip_minecart:
patch.write_token(APTokenTypes.WRITE, 0x3AC728, bytes([0x89, 0x13, 0x0, 0x10, 0xF, 0x08, 0x1]))
patch.write_token(APTokenTypes.WRITE, 0x3AC56C, bytes([0x49, 0x16, 0x0, 0x8, 0x8, 0x08, 0x1]))
if world.options.scale_stats:
patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1]))
if world.options.xp_multiplier:
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
if world.options.tattle_hp:
patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))
if world.options.music_options == 2:
patch.write_token(APTokenTypes.WRITE, 0x19B118, bytes([0x0, 0x25]))
if world.options.randomize_backgrounds:
all_enemies = Data.enemies + Data.bosses
for address in all_enemies:
patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)]))
for location_name in location_table.keys():
if (
(world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name)
or (world.options.castle_skip and "Bowser" in location_name)
or (world.options.disable_surf and "Surf Minigame" in location_name)
or (world.options.harhalls_pants and "Harhall's" in location_name)
):
continue
if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or (
world.options.chuckle_beans == 1 and location_table[location_name] in hidden
):
continue
if not world.options.coins and "Coin" in location_name:
continue
location = world.multiworld.get_location(location_name, world.player)
item = location.item
address = [address for address in all_locations if address.name == location.name]
item_inject(world, patch, location.address, address[0].itemType, item)
if "Shop" in location_name and "Coffee" not in location_name and item.player != world.player:
desc_inject(world, patch, location, item)
swap_colors(world, patch, world.options.mario_pants.value, 0, True)
swap_colors(world, patch, world.options.luigi_pants.value, 1, True)
swap_colors(world, patch, world.options.mario_color.value, 0)
swap_colors(world, patch, world.options.luigi_color.value, 1)
patch.write_file("token_data.bin", patch.get_token_binary())
def swap_colors(world: "MLSSWorld", patch: MLSSProcedurePatch, color: int, bro: int,
pants_option: Optional[bool] = False):
if not pants_option and color == bro:
return
chaos = False
if not pants_option and color == 11 or color == 12:
chaos = True
if pants_option and color == 11:
chaos = True
for c in [c for c in (cpants[color] if pants_option else colors[color])
if (c[3] == bro if not chaos else c[1] == bro)]:
if chaos:
patch.write_token(APTokenTypes.WRITE, c[0],
bytes([world.random.randint(0, 255), world.random.randint(0, 127)]))
else:
patch.write_token(APTokenTypes.WRITE, c[0], bytes([c[1], c[2]]))
def item_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: int, item_type: int, item: Item):
if item.player == world.player:
code = item_table[item.name].itemID
else:
code = 0x3F
if item_type == 0:
patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
elif item_type == 1:
if code == 0x1D or code == 0x1E:
code += 0xE
if 0x20 <= code <= 0x26:
code -= 0x4
insert = int(code)
insert2 = insert % 0x10
insert2 *= 0x10
insert //= 0x10
insert += 0x20
patch.write_token(APTokenTypes.WRITE, location, bytes([insert, insert2]))
elif item_type == 2:
if code == 0x1D or code == 0x1E:
code += 0xE
if 0x20 <= code <= 0x26:
code -= 0x4
patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
elif item_type == 3:
if code == 0x1D or code == 0x1E:
code += 0xE
if code < 0x1D:
code -= 0xA
if 0x20 <= code <= 0x26:
code -= 0xE
patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
else:
patch.write_token(APTokenTypes.WRITE, location, bytes([0x18]))
def desc_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: Location, item: Item):
index = -1
for key, value in shop.items():
if location.address in value:
if key == 0x3C05F0:
index = value.index(location.address)
else:
index = value.index(location.address) + 14
for key, value in badge.items():
if index != -1:
break
if location.address in value:
if key == 0x3C0618:
index = value.index(location.address) + 24
else:
index = value.index(location.address) + 41
for key, value in pants.items():
if index != -1:
break
if location.address in value:
if key == 0x3C0618:
index = value.index(location.address) + 48
else:
index = value.index(location.address) + 66
dstring = f"{world.multiworld.player_name[item.player]}: {item.name}"
patch.write_token(APTokenTypes.WRITE, 0xD11000 + (index * 0x40), dstring.encode("UTF8"))

571
worlds/mlss/Rules.py Normal file
View File

@@ -0,0 +1,571 @@
import typing
from worlds.generic.Rules import add_rule, forbid_item
from .Names.LocationName import LocationName
from .Locations import all_locations, hidden
from . import StateLogic
if typing.TYPE_CHECKING:
from . import MLSSWorld
def set_rules(world: "MLSSWorld", excluded):
for location in all_locations:
if "Digspot" in location.name:
if (world.options.skip_minecart and "Minecart" in location.name) or (
world.options.castle_skip and "Bowser" in location.name
):
continue
if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden:
continue
add_rule(
world.get_location(location.name),
lambda state: StateLogic.canDig(state, world.player),
)
if "Beanstone" in location.name:
add_rule(
world.get_location(location.name),
lambda state: StateLogic.canDig(state, world.player),
)
if "Shop" in location.name and "Coffee" not in location.name and location.name not in excluded:
forbid_item(world.get_location(location.name), "Hammers", world.player)
if "Badge" in location.name or "Pants" in location.name:
add_rule(
world.get_location(location.name),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
if location.itemType != 0 and location.name not in excluded:
if "Bowser" in location.name and world.options.castle_skip:
continue
forbid_item(world.get_location(location.name), "5 Coins", world.player)
if world.options.chuckle_beans == 2:
add_rule(
world.get_location(LocationName.HoohooVillageSuperHammerCaveDigspot),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsFarmRoomDigspot2),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsFarmRoomDigspot3),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsWhiteFruitRoomDigspot3),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndJojoraRoomDigspot),
lambda state: StateLogic.canDash(state, world.player),
)
if world.options.chuckle_beans != 0:
add_rule(
world.get_location(LocationName.HoohooMountainBaseBoostatueRoomDigspot2),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsFarmRoomDigspot1),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsWhiteFruitRoomDigspot2),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.TeeheeValleyPastUltraHammersDigspot1),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.TeeheeValleyPastUltraHammersDigspot3),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsNorthBeachDigspot3),
lambda state: StateLogic.canDash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsEDigspot2),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsNEDigspot1),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSRoom1Digspot2),
lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
)
forbid_item(
world.get_location(LocationName.SSChuckolaMembershipCard), "Nuts", world.player
) # Bandaid Fix
add_rule(
world.get_location(LocationName.HoohooVillageHammerHouseBlock),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBaseBoostatueRoomBlock2),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBooStatueMole),
lambda state: StateLogic.canMini(state, world.player) and StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooVillageSuperHammerCaveBlock),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsFarmRoomMoleReward1),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsFarmRoomMoleReward2),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsThunderHandMole),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsNWBlock),
lambda state: StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit1),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit2),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit3),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit4),
lambda state: StateLogic.super(state, world.player) and StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit5),
lambda state: StateLogic.super(state, world.player) and StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit6),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsBeanFruit7),
lambda state: StateLogic.teehee(state, world.player) and StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSRoom1Block),
lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSRoom2Block1),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock1),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock2),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock3),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock4),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleBlock),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSecretScroll1),
lambda state: StateLogic.thunder(state, world.player) and StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSecretScroll2),
lambda state: StateLogic.thunder(state, world.player) and StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooVillageMoleBehindTurtle),
lambda state: StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsNESoloMarioMole1),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsNESoloMarioMole2),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSuperHammerUpgrade),
lambda state: StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade),
lambda state: StateLogic.thunder(state, world.player)
and StateLogic.pieces(state, world.player)
and StateLogic.castleTown(state, world.player)
and StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRedChuckolaFruit),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsWhiteChuckolaFruit),
lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRoom7Block1),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRoom7Block2),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRoom4Block1),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRoom4Block2),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRoom4Block3),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsPipeRoomBlock1),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsPipeRoomBlock2),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock1),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock2),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock3),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock4),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock5),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastleFakeBeastar),
lambda state: StateLogic.pieces(state, world.player) and StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanCastlePeachsExtraDress),
lambda state: StateLogic.pieces(state, world.player) and StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.SewersRoom5Block1),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.SewersRoom5Block2),
lambda state: StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom1Block),
lambda state: StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2Block1),
lambda state: StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2Block2),
lambda state: StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonRedPearlBean),
lambda state: StateLogic.fire(state, world.player) and StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonGreenPearlBean),
lambda state: StateLogic.fire(state, world.player) and StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock1),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.TeeheeValleySoloLuigiMazeRoom1Block),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.OhoOasisFirebrand),
lambda state: StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.OhoOasisThunderhand),
lambda state: StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanstarPieceYoshiTheater),
lambda state: StateLogic.neon(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterAzureYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterBlueYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterGreenYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterOrangeYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterPurpleYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterRedYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.YoshiTheaterYellowYoshi),
lambda state: StateLogic.beanFruit(state, world.player),
)
add_rule(
world.get_location(LocationName.WinkleAreaBeanstarRoomBlock),
lambda state: StateLogic.winkle(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanstarPieceWinkleArea),
lambda state: StateLogic.winkle(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonSpangleReward),
lambda state: StateLogic.spangle(state, world.player),
)
add_rule(
world.get_location(LocationName.PantsShopMomPiranhaFlag1),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.PantsShopMomPiranhaFlag2),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.PantsShopMomPiranhaFlag3),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.BadgeShopMomPiranhaFlag1),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.BadgeShopMomPiranhaFlag2),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.BadgeShopMomPiranhaFlag3),
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.ChateauGreenGoblet),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDig(state, world.player),
)
add_rule(
world.get_location(LocationName.ChateauRedGoblet),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonSpangle),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonSpangleRoomBlock),
lambda state: StateLogic.ultra(state, world.player),
)
if world.options.difficult_logic:
add_rule(
world.get_location(LocationName.GwarharLagoonSpangleReward),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanstarPieceHermie),
lambda state: StateLogic.canCrash(state, world.player),
)
if world.options.chuckle_beans != 0:
add_rule(
world.get_location(LocationName.GwarharLagoonPastHermieDigspot),
lambda state: StateLogic.canCrash(state, world.player),
)
if world.options.coins:
add_rule(
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock2),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock3),
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsNWCoinBlock),
lambda state: StateLogic.super(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSRoom1CoinBlock),
lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.BeanbeanOutskirtsSRoom2CoinBlock),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.ChateauPoppleRoomCoinBlock1),
lambda state: StateLogic.brooch(state, world.player),
)
add_rule(
world.get_location(LocationName.ChateauPoppleRoomCoinBlock2),
lambda state: StateLogic.brooch(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsCaveRoom1CoinBlock),
lambda state: StateLogic.brooch(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsCaveRoom2CoinBlock),
lambda state: StateLogic.brooch(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsCaveRoom3CoinBlock),
lambda state: StateLogic.brooch(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsPipe5RoomCoinBlock),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsRoom7CoinBlock),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsKoopaRoomCoinBlock),
lambda state: StateLogic.brooch(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsWinkleAreaCaveCoinBlock),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.SewersPrisonRoomCoinBlock),
lambda state: StateLogic.rose(state, world.player),
)
add_rule(
world.get_location(LocationName.TeeheeValleyPastUltraHammerRocksCoinBlock),
lambda state: StateLogic.ultra(state, world.player),
)
add_rule(
world.get_location(LocationName.SSChuckolaStorageRoomCoinBlock1),
lambda state: StateLogic.super(state, world.player) or StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.SSChuckolaStorageRoomCoinBlock2),
lambda state: StateLogic.super(state, world.player) or StateLogic.canDash(state, world.player),
)
add_rule(
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock),
lambda state: StateLogic.canDash(state, world.player)
and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)),
)
add_rule(
world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock),
lambda state: StateLogic.ultra(state, world.player)
and StateLogic.fire(state, world.player)
and (
StateLogic.membership(state, world.player)
or (StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player))
),
)
add_rule(
world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock),
lambda state: StateLogic.ultra(state, world.player)
and StateLogic.fire(state, world.player)
and StateLogic.canDig(state, world.player)
and (StateLogic.membership(state, world.player) or StateLogic.canMini(state, world.player)),
)
if not world.options.difficult_logic:
add_rule(
world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock),
lambda state: StateLogic.canCrash(state, world.player),
)

155
worlds/mlss/StateLogic.py Normal file
View File

@@ -0,0 +1,155 @@
def canDig(state, player):
return state.has("Green Goblet", player) and state.has("Hammers", player)
def canMini(state, player):
return state.has("Red Goblet", player) and state.has("Hammers", player)
def canDash(state, player):
return state.has("Red Pearl Bean", player) and state.has("Firebrand", player)
def canCrash(state, player):
return state.has("Green Pearl Bean", player) and state.has("Thunderhand", player)
def hammers(state, player):
return state.has("Hammers", player)
def super(state, player):
return state.has("Hammers", player, 2)
def ultra(state, player):
return state.has("Hammers", player, 3)
def fruits(state, player):
return (
state.has("Red Chuckola Fruit", player)
and state.has("Purple Chuckola Fruit", player)
and state.has("White Chuckola Fruit", player)
)
def pieces(state, player):
return (
state.has("Beanstar Piece 1", player)
and state.has("Beanstar Piece 2", player)
and state.has("Beanstar Piece 3", player)
and state.has("Beanstar Piece 4", player)
)
def neon(state, player):
return (
state.has("Blue Neon Egg", player)
and state.has("Red Neon Egg", player)
and state.has("Green Neon Egg", player)
and state.has("Yellow Neon Egg", player)
and state.has("Purple Neon Egg", player)
and state.has("Orange Neon Egg", player)
and state.has("Azure Neon Egg", player)
)
def spangle(state, player):
return state.has("Spangle", player)
def rose(state, player):
return state.has("Peasley's Rose", player)
def brooch(state, player):
return state.has("Beanbean Brooch", player)
def thunder(state, player):
return state.has("Thunderhand", player)
def fire(state, player):
return state.has("Firebrand", player)
def dressBeanstar(state, player):
return state.has("Peach's Extra Dress", player) and state.has("Fake Beanstar", player)
def membership(state, player):
return state.has("Membership Card", player)
def winkle(state, player):
return state.has("Winkle Card", player)
def beanFruit(state, player):
return (
state.has("Bean Fruit 1", player)
and state.has("Bean Fruit 2", player)
and state.has("Bean Fruit 3", player)
and state.has("Bean Fruit 4", player)
and state.has("Bean Fruit 5", player)
and state.has("Bean Fruit 6", player)
and state.has("Bean Fruit 7", player)
)
def surfable(state, player):
return ultra(state, player) and (
(canDig(state, player) and canMini(state, player)) or (membership(state, player) and fire(state, player))
)
def postJokes(state, player):
return (
surfable(state, player)
and canDig(state, player)
and dressBeanstar(state, player)
and pieces(state, player)
and fruits(state, player)
and brooch(state, player)
and rose(state, player)
and canDash(state, player)
)
def teehee(state, player):
return super(state, player) or canDash(state, player)
def castleTown(state, player):
return fruits(state, player) and brooch(state, player)
def fungitown(state, player):
return (
castleTown(state, player)
and thunder(state, player)
and rose(state, player)
and (super(state, player) or canDash(state, player))
)
def piranha_shop(state, player):
return state.can_reach("Shop Mom Piranha Flag", "Region", player)
def fungitown_shop(state, player):
return state.can_reach("Shop Enter Fungitown Flag", "Region", player)
def star_shop(state, player):
return state.can_reach("Shop Beanstar Complete Flag", "Region", player)
def birdo_shop(state, player):
return state.can_reach("Shop Birdo Flag", "Region", player)
def fungitown_birdo_shop(state, player):
return state.can_reach("Fungitown Shop Birdo Flag", "Region", player)

183
worlds/mlss/__init__.py Normal file
View File

@@ -0,0 +1,183 @@
import os
import pkgutil
import typing
import settings
from BaseClasses import Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World
from typing import List, Dict, Any
from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins
from .Options import MLSSOptions
from .Items import MLSSItem, itemList, item_frequencies, item_table
from .Names.LocationName import LocationName
from .Client import MLSSClient
from .Regions import create_regions, connect_regions
from .Rom import MLSSProcedurePatch, write_tokens
from .Rules import set_rules
class MLSSWebWorld(WebWorld):
theme = "partyTime"
bug_report_page = "https://github.com/jamesbrq/ArchipelagoMLSS/issues"
tutorials = [
Tutorial(
tutorial_name="Setup Guide",
description="A guide to setting up Mario & Luigi: Superstar Saga for Archipelago.",
language="English",
file_name="setup_en.md",
link="setup/en",
authors=["jamesbrq"],
)
]
class MLSSSettings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the MLSS US rom"""
copy_to = "Mario & Luigi - Superstar Saga (U).gba"
description = "MLSS ROM File"
md5s = ["4b1a5897d89d9e74ec7f630eefdfd435"]
rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: bool = True
class MLSSWorld(World):
"""
Adventure with Mario and Luigi together in the Beanbean Kingdom
to stop the evil Cackletta and retrieve the Beanstar.
"""
game = "Mario & Luigi Superstar Saga"
web = MLSSWebWorld()
options_dataclass = MLSSOptions
options: MLSSOptions
settings: typing.ClassVar[MLSSSettings]
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
required_client_version = (0, 4, 5)
disabled_locations: List[str]
def generate_early(self) -> None:
self.disabled_locations = []
if self.options.chuckle_beans == 0:
self.disabled_locations += [location.name for location in all_locations if "Digspot" in location.name]
if self.options.castle_skip:
self.disabled_locations += [location.name for location in all_locations if "Bowser" in location.name]
if self.options.chuckle_beans == 1:
self.disabled_locations = [location.name for location in all_locations if location.id in hidden]
if self.options.skip_minecart:
self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot]
if self.options.disable_surf:
self.disabled_locations += [LocationName.SurfMinigame]
if self.options.harhalls_pants:
self.disabled_locations += [LocationName.HarhallsPants]
if not self.options.coins:
self.disabled_locations += [location.name for location in all_locations if location in coins]
def create_regions(self) -> None:
create_regions(self, self.disabled_locations)
connect_regions(self)
item = self.create_item("Mushroom")
self.get_location(LocationName.ShopStartingFlag1).place_locked_item(item)
item = self.create_item("Syrup")
self.get_location(LocationName.ShopStartingFlag2).place_locked_item(item)
item = self.create_item("1-UP Mushroom")
self.get_location(LocationName.ShopStartingFlag3).place_locked_item(item)
item = self.create_item("Hoo Bean")
self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item)
item = self.create_item("Chuckle Bean")
self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item)
def fill_slot_data(self) -> Dict[str, Any]:
return {
"CastleSkip": self.options.castle_skip.value,
"SkipMinecart": self.options.skip_minecart.value,
"DisableSurf": self.options.disable_surf.value,
"HarhallsPants": self.options.harhalls_pants.value,
"ChuckleBeans": self.options.chuckle_beans.value,
"DifficultLogic": self.options.difficult_logic.value,
"Coins": self.options.coins.value,
}
def create_items(self) -> None:
# First add in all progression and useful items
required_items = []
precollected = [item for item in itemList if item in self.multiworld.precollected_items]
for item in itemList:
if item.classification != ItemClassification.filler and item.classification != ItemClassification.skip_balancing:
freq = item_frequencies.get(item.itemName, 1)
if item in precollected:
freq = max(freq - precollected.count(item), 0)
if self.options.harhalls_pants and "Harhall's" in item.itemName:
continue
required_items += [item.itemName for _ in range(freq)]
for itemName in required_items:
self.multiworld.itempool.append(self.create_item(itemName))
# Then, create our list of filler items
filler_items = []
for item in itemList:
if item.classification != ItemClassification.filler:
continue
if item.itemName == "5 Coins" and not self.options.coins:
continue
freq = item_frequencies.get(item.itemName, 1)
if self.options.chuckle_beans == 0:
if item.itemName == "Chuckle Bean":
continue
if self.options.chuckle_beans == 1:
if item.itemName == "Chuckle Bean":
freq -= 59
filler_items += [item.itemName for _ in range(freq)]
# And finally take as many fillers as we need to have the same amount of items and locations.
remaining = len(all_locations) - len(required_items) - 5
if self.options.castle_skip:
remaining -= len(bowsers) + len(bowsersMini) - (5 if self.options.chuckle_beans == 0 else 0)
if self.options.skip_minecart and self.options.chuckle_beans == 2:
remaining -= 1
if self.options.disable_surf:
remaining -= 1
if self.options.harhalls_pants:
remaining -= 1
if self.options.chuckle_beans == 0:
remaining -= 192
if self.options.chuckle_beans == 1:
remaining -= 59
if not self.options.coins:
remaining -= len(coins)
self.multiworld.itempool += [
self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining)
]
def set_rules(self) -> None:
set_rules(self, self.disabled_locations)
if self.options.castle_skip:
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
"PostJokes", "Region", self.player
)
else:
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
"Bowser's Castle Mini", "Region", self.player
)
def create_item(self, name: str) -> MLSSItem:
item = item_table[name]
return MLSSItem(item.itemName, item.classification, item.code, self.player)
def get_filler_item_name(self) -> str:
return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList)))
def generate_output(self, output_directory: str) -> None:
patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/basepatch.bsdiff"))
write_tokens(self, patch)
rom_path = os.path.join(
output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" f"{patch.patch_file_ending}"
)
patch.write(rom_path)

Binary file not shown.

View File

@@ -0,0 +1,66 @@
# Mario & Luigi: Superstar Saga
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and
export a config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle, the player may need to access certain areas before they
would in the vanilla game.
The game has been changed to an open-world style as opposed to the linear style the vanilla game has.
Other Features such as Turbo through textboxes (Hold L/R+A) and Pipe Warping from any room (Hold L+R+SELECT) have been added for convenience.
Enemies and Bosses can be randomized, and their stats can be scaled to feel more like the vanilla game's stats.
Other aspects of the game can be randomized as well such as music, sounds, battle backgrounds, Mario and Luigi's Colors, and more.
## What is the goal of Mario & Luigi: Superstar Saga when randomized?
Defeat Cackletta's Soul in Bowser's Castle. This requires you to collect all 4 Beanstar Pieces, restore the Beanstar, and bring Peach's Extra Dress and the Fake Beanstar to Fawful at the end of Jokes End.
In total, this requires:
- 4 Beanstar Pieces
- Peach's Extra Dress
- Fake Beanstar
- Ultra Hammers
- Fire Hand
- Thunder Hand
- Red Pearl Bean
- Green Pearl Bean
- Green Goblet
- Peasley's Rose
- Beanbean Brooch
- All 3 Chuckola Fruits
- Membership Card OR Red Goblet
## What items and locations can get shuffled?
Locations in which items can be found:
- All Item Blocks and Coin Blocks
- All Chuckle Bean Digspots
- All Shop items
- All Pants and Badge shop items
- All Espresso brews and rewards
- All Minigame Rewards
- All Event based items
Items that can be shuffled:
- All consumable items (Mushrooms, Nuts, Syrups etc.)
- All Hoo Beans and Chuckle Beans
- All Badges and Pants
- All key items (Beanfruits, Beanbean Brooch, Hammers etc.)
- All Extra Gears (Great Force, Gameboy Horror SP etc.)
## What does another world's item look like in Mario & Luigi: Superstar Saga?
Items will show up as a Golden Mushroom from boxes and Digspots and "AP Item" in all textboxes.
Items in a shop from another player's world will display the player name and item name in addition to being displayed as an AP Item.
## When the player receives an item, what happens?
Items will be placed directly into the players inventory after a few seconds. Sometimes for certain events and cutscenes to be properly triggered right after you received an item, you may have to leave and re-enter the room to properly load everything required for the respective event or cutscene.

View File

@@ -0,0 +1,53 @@
# Setup Guide for Mario & Luigi: Superstar Saga Archipelago
## Important
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
## Required Software
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.9.1 is recommended.
- Detailed installation instructions for Bizhawk can be found at the above link.
- Windows users must run the prerequisite installer first, which can also be found at the above link.
- The built-in Bizhawk client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
- A US copy of Mario & Luigi: Superstar Saga
## Optional Software
- [Poptracker](https://github.com/black-sliver/PopTracker/releases)
- [MLSS Autotracker](https://github.com/seto10987/MLSS-PopTracker/releases)
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
You can customize your options by visiting the
[Mario & Luigi Superstar Saga Options Page](/games/Mario%20&%20Luigi%20Superstar%20Saga/player-options)
## Joining a MultiWorld Game
### Obtain your GBA patch file
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
files. Your data file should have a `.apmlss` extension.
Double-click on your `.apmlss` file to start your client and start the ROM patch process. Once the process is finished, the client and the emulator will be started automatically (if you associated the extension
to the emulator as recommended).
### Connect to the Multiserver
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)

View File

@@ -12,7 +12,7 @@ class NoitaLocation(Location):
class LocationData(NamedTuple):
id: int
flag: int = 0
ltype: str = "shop"
ltype: str = "Shop"
class LocationFlag(IntEnum):
@@ -25,7 +25,7 @@ class LocationFlag(IntEnum):
# Mapping of items in each region.
# Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions.
# ltype key: "chest" = Hidden Chests, "pedestal" = Pedestals, "boss" = Boss, "orb" = Orb.
# ltype key: "Chest" = Hidden Chests, "Pedestal" = Pedestals, "Boss" = Boss, "Orb" = Orb.
# 110000-110671
location_region_mapping: Dict[str, Dict[str, LocationData]] = {
"Coal Pits Holy Mountain": {
@@ -91,117 +91,118 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
"Secret Shop Item 4": LocationData(110045),
},
"The Sky": {
"Kivi": LocationData(110670, LocationFlag.main_world, "boss"),
"Kivi": LocationData(110670, LocationFlag.main_world, "Boss"),
},
"Floating Island": {
"Floating Island Orb": LocationData(110658, LocationFlag.main_path, "orb"),
"Floating Island Orb": LocationData(110658, LocationFlag.main_path, "Orb"),
},
"Pyramid": {
"Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "boss"),
"Pyramid Orb": LocationData(110659, LocationFlag.main_world, "orb"),
"Sandcave Orb": LocationData(110662, LocationFlag.main_world, "orb"),
"Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "Boss"),
"Pyramid Orb": LocationData(110659, LocationFlag.main_world, "Orb"),
"Sandcave Orb": LocationData(110662, LocationFlag.main_world, "Orb"),
},
"Overgrown Cavern": {
"Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "chest"),
"Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "pedestal"),
"Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "Chest"),
"Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "Pedestal"),
},
"Lake": {
"Syväolento": LocationData(110651, LocationFlag.main_world, "boss"),
"Tapion vasalli": LocationData(110669, LocationFlag.main_world, "boss"),
"Syväolento": LocationData(110651, LocationFlag.main_world, "Boss"),
"Tapion vasalli": LocationData(110669, LocationFlag.main_world, "Boss"),
},
"Frozen Vault": {
"Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "orb"),
"Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "chest"),
"Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "pedestal"),
"Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "Orb"),
"Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "Chest"),
"Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "Pedestal"),
},
"Mines": {
"Mines Chest": LocationData(110046, LocationFlag.main_path, "chest"),
"Mines Pedestal": LocationData(110066, LocationFlag.main_path, "pedestal"),
"Mines Chest": LocationData(110046, LocationFlag.main_path, "Chest"),
"Mines Pedestal": LocationData(110066, LocationFlag.main_path, "Pedestal"),
},
# Collapsed Mines is a very small area, combining it with the Mines. Leaving this here as a reminder
"Ancient Laboratory": {
"Ylialkemisti": LocationData(110656, LocationFlag.side_path, "boss"),
"Ylialkemisti": LocationData(110656, LocationFlag.side_path, "Boss"),
},
"Abyss Orb Room": {
"Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "boss"),
"Abyss Orb": LocationData(110665, LocationFlag.main_path, "orb"),
"Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "Boss"),
"Abyss Orb": LocationData(110665, LocationFlag.main_path, "Orb"),
},
"Below Lava Lake": {
"Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "orb"),
"Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "Orb"),
},
"Coal Pits": {
"Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "chest"),
"Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "pedestal"),
"Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "Chest"),
"Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "Pedestal"),
},
"Fungal Caverns": {
"Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "chest"),
"Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "pedestal"),
"Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "Chest"),
"Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "Pedestal"),
},
"Snowy Depths": {
"Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "chest"),
"Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "pedestal"),
"Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "Chest"),
"Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "Pedestal"),
},
"Magical Temple": {
"Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "orb"),
"Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "Orb"),
},
"Hiisi Base": {
"Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "chest"),
"Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "pedestal"),
"Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "Chest"),
"Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "Pedestal"),
},
"Underground Jungle": {
"Suomuhauki": LocationData(110648, LocationFlag.main_path, "boss"),
"Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "chest"),
"Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "pedestal"),
"Suomuhauki": LocationData(110648, LocationFlag.main_path, "Boss"),
"Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "Chest"),
"Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "Pedestal"),
},
"Lukki Lair": {
"Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "orb"),
"Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "chest"),
"Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "pedestal"),
"Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "Orb"),
"Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "Chest"),
"Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "Pedestal"),
},
"The Vault": {
"The Vault Chest": LocationData(110366, LocationFlag.main_path, "chest"),
"The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "pedestal"),
"The Vault Chest": LocationData(110366, LocationFlag.main_path, "Chest"),
"The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "Pedestal"),
},
"Temple of the Art": {
"Gate Guardian": LocationData(110652, LocationFlag.main_path, "boss"),
"Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "chest"),
"Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "pedestal"),
"Gate Guardian": LocationData(110652, LocationFlag.main_path, "Boss"),
"Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "Chest"),
"Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "Pedestal"),
},
"The Tower": {
"The Tower Chest": LocationData(110606, LocationFlag.main_world, "chest"),
"The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "pedestal"),
"The Tower Chest": LocationData(110606, LocationFlag.main_world, "Chest"),
"The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "Pedestal"),
},
"Wizards' Den": {
"Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "boss"),
"Wizards' Den Orb": LocationData(110668, LocationFlag.main_world, "orb"),
"Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "chest"),
"Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "pedestal"),
"Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "Boss"),
"Wizards' Den Orb": LocationData(110668, LocationFlag.main_world, "Orb"),
"Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "Chest"),
"Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "Pedestal"),
},
"Powerplant": {
"Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "boss"),
"Power Plant Chest": LocationData(110486, LocationFlag.main_world, "chest"),
"Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "pedestal"),
"Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "Boss"),
"Power Plant Chest": LocationData(110486, LocationFlag.main_world, "Chest"),
"Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "Pedestal"),
},
"Snow Chasm": {
"Unohdettu": LocationData(110653, LocationFlag.main_world, "boss"),
"Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "orb"),
"Unohdettu": LocationData(110653, LocationFlag.main_world, "Boss"),
"Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "Orb"),
},
"Meat Realm": {
"Meat Realm Chest": LocationData(110086, LocationFlag.main_world, "chest"),
"Meat Realm Pedestal": LocationData(110106, LocationFlag.main_world, "pedestal"),
"Limatoukka": LocationData(110647, LocationFlag.main_world, "boss"),
"Meat Realm Chest": LocationData(110086, LocationFlag.main_world, "Chest"),
"Meat Realm Pedestal": LocationData(110106, LocationFlag.main_world, "Pedestal"),
"Limatoukka": LocationData(110647, LocationFlag.main_world, "Boss"),
},
"West Meat Realm": {
"Kolmisilmän sydän": LocationData(110671, LocationFlag.main_world, "boss"),
"Kolmisilmän sydän": LocationData(110671, LocationFlag.main_world, "Boss"),
},
"The Laboratory": {
"Kolmisilmä": LocationData(110646, LocationFlag.main_path, "boss"),
"Kolmisilmä": LocationData(110646, LocationFlag.main_path, "Boss"),
},
"Friend Cave": {
"Toveri": LocationData(110654, LocationFlag.main_world, "boss"),
"Toveri": LocationData(110654, LocationFlag.main_world, "Boss"),
},
"The Work (Hell)": {
"The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "orb"),
"The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "Orb"),
},
}
@@ -212,18 +213,20 @@ def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str,
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
"pedestal": set()}
location_name_groups: Dict[str, Set[str]] = {"Shop": set(), "Orb": set(), "Boss": set(), "Chest": set(),
"Pedestal": set()}
location_name_to_id: Dict[str, int] = {}
for location_group in location_region_mapping.values():
for region_name, location_group in location_region_mapping.items():
location_name_groups[region_name] = set()
for locname, locinfo in location_group.items():
# Iterating the hidden chest and pedestal locations here to avoid clutter above
amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1
amount = 20 if locinfo.ltype in ["Chest", "Pedestal"] else 1
entries = make_location_range(locname, locinfo.id, amount)
location_name_to_id.update(entries)
location_name_groups[locinfo.ltype].update(entries.keys())
location_name_groups[region_name].update(entries.keys())
shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}

View File

@@ -15,14 +15,14 @@ def create_locations(world: "NoitaWorld", region: Region) -> None:
location_type = location_data.ltype
flag = location_data.flag
is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks
is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks
is_orb_allowed = location_type == "Orb" and flag <= world.options.orbs_as_checks
is_boss_allowed = location_type == "Boss" and flag <= world.options.bosses_as_checks
amount = 0
if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
amount = 1
elif location_type == "chest" and flag <= world.options.path_option:
elif location_type == "Chest" and flag <= world.options.path_option:
amount = world.options.hidden_chests.value
elif location_type == "pedestal" and flag <= world.options.path_option:
elif location_type == "Pedestal" and flag <= world.options.path_option:
amount = world.options.pedestal_checks.value
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),

View File

@@ -1,3 +1,13 @@
# 2.1.1
### Features
- You no longer need a copy of Pokemon Emerald to generate a game, patch files generate much faster.
# 2.1.0
_Separately released, branching from 2.0.0. Included procedure patch migration, but none of the 2.0.1 fixes._
# 2.0.1
### Fixes

View File

@@ -5,11 +5,12 @@ from collections import Counter
import copy
import logging
import os
import pkgutil
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union
from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
from Fill import FillError, fill_restrictive
from Options import Toggle
from Options import OptionError, Toggle
import settings
from worlds.AutoWorld import WebWorld, World
@@ -25,7 +26,7 @@ from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType,
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)
from .rom import PokemonEmeraldDeltaPatch, create_patch
from .rom import PokemonEmeraldProcedurePatch, write_tokens
class PokemonEmeraldWebWorld(WebWorld):
@@ -60,7 +61,7 @@ class PokemonEmeraldSettings(settings.Group):
"""File name of your English Pokemon Emerald ROM"""
description = "Pokemon Emerald ROM File"
copy_to = "Pokemon - Emerald Version (USA, Europe).gba"
md5s = [PokemonEmeraldDeltaPatch.hash]
md5s = [PokemonEmeraldProcedurePatch.hash]
rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to)
@@ -126,9 +127,6 @@ class PokemonEmeraldWorld(World):
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
from .sanity_check import validate_regions
if not os.path.exists(cls.settings.rom_file):
raise FileNotFoundError(cls.settings.rom_file)
assert validate_regions()
def get_filler_item_name(self) -> str:
@@ -183,8 +181,8 @@ class PokemonEmeraldWorld(World):
if self.options.goal == Goal.option_legendary_hunt:
# Prevent turning off all legendary encounters
if len(self.options.allowed_legendary_hunt_encounters.value) == 0:
raise ValueError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) "
"needs to allow at least one legendary encounter when goal is legendary hunt.")
raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) "
"needs to allow at least one legendary encounter when goal is legendary hunt.")
# Prevent setting the number of required legendaries higher than the number of enabled legendaries
if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value):
@@ -195,8 +193,8 @@ class PokemonEmeraldWorld(World):
# Require random wild encounters if dexsanity is enabled
if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
raise ValueError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) must "
"not leave wild encounters vanilla if enabling dexsanity.")
raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) must "
"not leave wild encounters vanilla if enabling dexsanity.")
# If badges or HMs are vanilla, Norman locks you from using Surf,
# which means you're not guaranteed to be able to reach Fortree Gym,
@@ -591,7 +589,9 @@ class PokemonEmeraldWorld(World):
randomize_opponent_parties(self)
randomize_starters(self)
create_patch(self, output_directory)
patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4"))
write_tokens(self, patch)
del self.modified_trainers
del self.modified_tmhm_moves
@@ -600,6 +600,10 @@ class PokemonEmeraldWorld(World):
del self.modified_starters
del self.modified_species
# Write Output
out_file_name = self.multiworld.get_out_file_name_base(self.player)
patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))
def write_spoiler(self, spoiler_handle: TextIO):
if self.options.dexsanity:
from collections import defaultdict

View File

@@ -51,6 +51,13 @@ TRACKER_EVENT_FLAGS = [
"FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt)
"FLAG_IS_CHAMPION",
"FLAG_PURCHASED_HARBOR_MAIL",
"FLAG_REGI_DOORS_OPENED",
"FLAG_RETURNED_DEVON_GOODS",
"FLAG_DOCK_REJECTED_DEVON_GOODS",
"FLAG_DEFEATED_EVIL_TEAM_MT_CHIMNEY",
"FLAG_WINGULL_SENT_ON_ERRAND",
"FLAG_WINGULL_DELIVERED_MAIL",
"FLAG_MET_PRETTY_PETAL_SHOP_OWNER",
]
EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS}
@@ -84,6 +91,10 @@ KEY_LOCATION_FLAGS = [
"NPC_GIFT_RECEIVED_OLD_ROD",
"NPC_GIFT_RECEIVED_GOOD_ROD",
"NPC_GIFT_RECEIVED_SUPER_ROD",
"NPC_GIFT_RECEIVED_EON_TICKET",
"NPC_GIFT_RECEIVED_AURORA_TICKET",
"NPC_GIFT_RECEIVED_MYSTIC_TICKET",
"NPC_GIFT_RECEIVED_OLD_SEA_MAP",
]
KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS}

View File

@@ -289,6 +289,7 @@ class TrainerData:
party: TrainerPartyData
address: int
script_address: int
battle_type: int
class PokemonEmeraldData:
@@ -1422,7 +1423,8 @@ def _init() -> None:
trainer_json["party_address"]
),
trainer_json["address"],
trainer_json["script_address"]
trainer_json["script_address"],
trainer_json["battle_type"]
))

File diff suppressed because one or more lines are too long

View File

@@ -1269,7 +1269,7 @@
"REGION_SLATEPORT_CITY/MAIN": {
"parent_map": "MAP_SLATEPORT_CITY",
"has_grass": false,
"has_water": true,
"has_water": false,
"has_fishing": true,
"locations": [
"NPC_GIFT_RECEIVED_POWDER_JAR"
@@ -1279,9 +1279,9 @@
"EVENT_VISITED_SLATEPORT_CITY"
],
"exits": [
"REGION_SLATEPORT_CITY/WATER",
"REGION_ROUTE109/BEACH",
"REGION_ROUTE110/SOUTH",
"REGION_ROUTE134/WEST"
"REGION_ROUTE110/SOUTH"
],
"warps": [
"MAP_SLATEPORT_CITY:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0",
@@ -1296,6 +1296,19 @@
"MAP_SLATEPORT_CITY:10/MAP_SLATEPORT_CITY_HOUSE:0"
]
},
"REGION_SLATEPORT_CITY/WATER": {
"parent_map": "MAP_SLATEPORT_CITY",
"has_grass": false,
"has_water": true,
"has_fishing": true,
"locations": [],
"events": [],
"exits": [
"REGION_SLATEPORT_CITY/MAIN",
"REGION_ROUTE134/WEST"
],
"warps": []
},
"REGION_SLATEPORT_CITY_POKEMON_CENTER_2F/MAIN": {
"parent_map": "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F",
"has_grass": false,

View File

@@ -3294,7 +3294,7 @@
"locations": [],
"events": [],
"exits": [
"REGION_SLATEPORT_CITY/MAIN"
"REGION_SLATEPORT_CITY/WATER"
],
"warps": []
},

View File

@@ -21,7 +21,7 @@ clear it.
## Optional Software
- [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest), for use with
- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Generating and Patching a Game
@@ -64,7 +64,7 @@ perfectly safe to make progress offline; everything will re-sync when you reconn
Pokémon Emerald has a fully functional map tracker that supports auto-tracking.
1. Download [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest) and
1. Download [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) and
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Put the tracker pack into packs/ in your PopTracker install.
3. Open PopTracker, and load the Pokémon Emerald pack.

View File

@@ -21,7 +21,7 @@ limpiarlas, selecciona el atajo y presiona la tecla Esc.
## Software Opcional
- [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest), para usar con
- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar con
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Generando y Parcheando el Juego
@@ -65,7 +65,7 @@ jugar de manera offline; se sincronizará todo cuando te vuelvas a conectar.
Pokémon Emerald tiene un Map Tracker completamente funcional que soporta auto-tracking.
1. Descarga [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest) y
1. Descarga [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) y
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Coloca la carpeta del Tracker en la carpeta packs/ dentro de la carpeta de instalación del PopTracker.
3. Abre PopTracker, y carga el Pack de Pokémon Emerald Map Tracker.

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Dict, Optional, FrozenSet, Iterable
from BaseClasses import Location, Region
from .data import BASE_OFFSET, POKEDEX_OFFSET, data
from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, data
from .items import offset_item_value
if TYPE_CHECKING:
@@ -130,8 +130,14 @@ def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str,
location_data = data.locations[location_name]
location_id = offset_flag(location_data.flag)
if location_data.flag == 0:
location_id += POKEDEX_OFFSET + int(location_name[15:])
if location_data.flag == 0: # Dexsanity location
national_dex_id = int(location_name[-3:]) # Location names are formatted POKEDEX_REWARD_###
# Don't create this pokedex location if player can't find it in the wild
if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds:
continue
location_id += POKEDEX_OFFSET + national_dex_id
location = PokemonEmeraldLocation(
world.player,

View File

@@ -242,9 +242,9 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
RandomizeWildPokemon.option_match_type,
RandomizeWildPokemon.option_match_base_stats_and_type,
}
catch_em_all = world.options.dexsanity == Toggle.option_true
catch_em_all_placed = set()
already_placed = set()
num_placeable_species = NUM_REAL_SPECIES - len(world.blacklisted_wilds)
priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]]
@@ -290,8 +290,8 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
# If dexsanity/catch 'em all mode, blacklist already placed species
# until every species has been placed once
if catch_em_all and len(catch_em_all_placed) < NUM_REAL_SPECIES:
blacklists[1].append(catch_em_all_placed)
if world.options.dexsanity and len(already_placed) < num_placeable_species:
blacklists[1].append(already_placed)
# Blacklist from player options
blacklists[2].append(world.blacklisted_wilds)
@@ -329,8 +329,8 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
new_species_id = world.random.choice(candidates).species_id
species_old_to_new_map[species_id] = new_species_id
if catch_em_all and map_data.name not in POSTGAME_MAPS:
catch_em_all_placed.add(new_species_id)
if world.options.dexsanity and map_data.name not in POSTGAME_MAPS:
already_placed.add(new_species_id)
# Actually create the new list of slots and encounter table
new_slots: List[int] = []

View File

@@ -3,12 +3,10 @@ Classes and functions related to creating a ROM patch
"""
import copy
import os
import pkgutil
import struct
from typing import TYPE_CHECKING, Dict, List, Tuple
import bsdiff4
from worlds.Files import APDeltaPatch
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
from settings import get_settings
from .data import TrainerPokemonDataTypeEnum, BASE_OFFSET, data
@@ -96,38 +94,32 @@ CAVE_EVENT_NAME_TO_ID = {
}
def _set_bytes_le(byte_array: bytearray, address: int, size: int, value: int) -> None:
offset = 0
while size > 0:
byte_array[address + offset] = value & 0xFF
value = value >> 8
offset += 1
size -= 1
class PokemonEmeraldDeltaPatch(APDeltaPatch):
class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin):
game = "Pokemon Emerald"
hash = "605b89b67018abcea91e693a4dd25be3"
patch_file_ending = ".apemerald"
result_file_ending = ".gba"
procedure = [
("apply_bsdiff4", ["base_patch.bsdiff4"]),
("apply_tokens", ["token_data.bin"])
]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_as_bytes()
with open(get_settings().pokemon_emerald_settings.rom_file, "rb") as infile:
base_rom_bytes = bytes(infile.read())
return base_rom_bytes
def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
base_rom = get_base_rom_as_bytes()
base_patch = pkgutil.get_data(__name__, "data/base_patch.bsdiff4")
patched_rom = bytearray(bsdiff4.patch(base_rom, base_patch))
def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
# Set free fly location
if world.options.free_fly_location:
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoOptions"] + 0x20,
1,
world.free_fly_location_id
struct.pack("<B", world.free_fly_location_id)
)
location_info: List[Tuple[int, int, str]] = []
@@ -141,26 +133,32 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
# Set local item values
if not world.options.remote_items and location.item.player == world.player:
if type(location.item_address) is int:
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
location.item_address,
2,
reverse_offset_item_value(location.item.code)
struct.pack("<H", location.item.code - BASE_OFFSET)
)
elif type(location.item_address) is list:
for address in location.item_address:
_set_bytes_le(patched_rom, address, 2, reverse_offset_item_value(location.item.code))
patch.write_token(
APTokenTypes.WRITE,
address,
struct.pack("<H", location.item.code - BASE_OFFSET)
)
else:
if type(location.item_address) is int:
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
location.item_address,
2,
data.constants["ITEM_ARCHIPELAGO_PROGRESSION"]
struct.pack("<H", data.constants["ITEM_ARCHIPELAGO_PROGRESSION"])
)
elif type(location.item_address) is list:
for address in location.item_address:
_set_bytes_le(patched_rom, address, 2, data.constants["ITEM_ARCHIPELAGO_PROGRESSION"])
patch.write_token(
APTokenTypes.WRITE,
address,
struct.pack("<H", data.constants["ITEM_ARCHIPELAGO_PROGRESSION"])
)
# Creates a list of item information to store in tables later. Those tables are used to display the item and
# player name in a text box. In the case of not enough space, the game will default to "found an ARCHIPELAGO
@@ -194,9 +192,21 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
# message (the message for receiving an item will pop up when the client eventually gives it to them).
# In race mode, no item location data is included, and only recieved (or own) items will show any text box.
if item_player == world.player or world.multiworld.is_race:
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0, 2, flag)
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 2, 2, 0)
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 4, 1, 0)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0,
struct.pack("<H", flag)
)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 2,
struct.pack("<H", 0)
)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 4,
struct.pack("<B", 0)
)
else:
player_name = world.multiworld.player_name[item_player]
@@ -207,11 +217,10 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
player_name_ids[player_name] = len(player_name_ids)
for j, b in enumerate(encode_string(player_name, 17)):
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoPlayerNames"] + (player_name_ids[player_name] * 17) + j,
1,
b
struct.pack("<B", b)
)
if item_name not in item_name_offsets:
@@ -224,18 +233,28 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
item_name_offsets[item_name] = next_item_name_offset
next_item_name_offset += len(item_name) + 1
for j, b in enumerate(encode_string(item_name) + b"\xFF"):
_set_bytes_le(
patched_rom,
data.rom_addresses["gArchipelagoItemNames"] + (item_name_offsets[item_name]) + j,
1,
b
)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoItemNames"] + (item_name_offsets[item_name]),
encode_string(item_name) + b"\xFF"
)
# There should always be enough space for one entry per location
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0, 2, flag)
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 2, 2, item_name_offsets[item_name])
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 4, 1, player_name_ids[player_name])
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0,
struct.pack("<H", flag)
)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 2,
struct.pack("<H", item_name_offsets[item_name])
)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 4,
struct.pack("<B", player_name_ids[player_name])
)
easter_egg = get_easter_egg(world.options.easter_egg.value)
@@ -282,40 +301,40 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
for i, slot in enumerate(pc_slots):
address = data.rom_addresses["sNewGamePCItems"] + (i * 4)
item = reverse_offset_item_value(world.item_name_to_id[slot[0]])
_set_bytes_le(patched_rom, address + 0, 2, item)
_set_bytes_le(patched_rom, address + 2, 2, slot[1])
patch.write_token(APTokenTypes.WRITE, address + 0, struct.pack("<H", item))
patch.write_token(APTokenTypes.WRITE, address + 2, struct.pack("<H", slot[1]))
# Set species data
_set_species_info(world, patched_rom, easter_egg)
_set_species_info(world, patch, easter_egg)
# Set encounter tables
if world.options.wild_pokemon != RandomizeWildPokemon.option_vanilla:
_set_encounter_tables(world, patched_rom)
_set_encounter_tables(world, patch)
# Set opponent data
if world.options.trainer_parties != RandomizeTrainerParties.option_vanilla or easter_egg[0] == 2:
_set_opponents(world, patched_rom, easter_egg)
_set_opponents(world, patch, easter_egg)
# Set legendary pokemon
_set_legendary_encounters(world, patched_rom)
_set_legendary_encounters(world, patch)
# Set misc pokemon
_set_misc_pokemon(world, patched_rom)
_set_misc_pokemon(world, patch)
# Set starters
_set_starters(world, patched_rom)
_set_starters(world, patch)
# Set TM moves
_set_tm_moves(world, patched_rom, easter_egg)
_set_tm_moves(world, patch, easter_egg)
# Randomize move tutor moves
_randomize_move_tutor_moves(world, patched_rom, easter_egg)
_randomize_move_tutor_moves(world, patch, easter_egg)
# Set TM/HM compatibility
_set_tmhm_compatibility(world, patched_rom)
_set_tmhm_compatibility(world, patch)
# Randomize opponent double or single
_randomize_opponent_battle_type(world, patched_rom)
_randomize_opponent_battle_type(world, patch)
# Options
# struct ArchipelagoOptions
@@ -360,73 +379,118 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
options_address = data.rom_addresses["gArchipelagoOptions"]
# Set Birch pokemon
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x00,
2,
world.random.choice(list(data.species.keys()))
struct.pack("<H", world.random.choice(list(data.species.keys())))
)
# Set hold A to advance text
_set_bytes_le(patched_rom, options_address + 0x02, 1, 1 if world.options.turbo_a else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x02,
struct.pack("<B", 1 if world.options.turbo_a else 0)
)
# Set receive item messages type
_set_bytes_le(patched_rom, options_address + 0x03, 1, world.options.receive_item_messages.value)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x03,
struct.pack("<B", world.options.receive_item_messages.value)
)
# Set better shops
_set_bytes_le(patched_rom, options_address + 0x04, 1, 1 if world.options.better_shops else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x04,
struct.pack("<B", 1 if world.options.better_shops else 0)
)
# Set reusable TMs
_set_bytes_le(patched_rom, options_address + 0x05, 1, 1 if world.options.reusable_tms_tutors else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x05,
struct.pack("<B", 1 if world.options.reusable_tms_tutors else 0)
)
# Set guaranteed catch
_set_bytes_le(patched_rom, options_address + 0x06, 1, 1 if world.options.guaranteed_catch else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x06,
struct.pack("<B", 1 if world.options.guaranteed_catch else 0)
)
# Set purge spinners
_set_bytes_le(patched_rom, options_address + 0x07, 1, 1 if world.options.purge_spinners else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x07,
struct.pack("<B", 1 if world.options.purge_spinners else 0)
)
# Set blind trainers
_set_bytes_le(patched_rom, options_address + 0x08, 1, 1 if world.options.blind_trainers else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x08,
struct.pack("<B", 1 if world.options.blind_trainers else 0)
)
# Set exp modifier
_set_bytes_le(patched_rom, options_address + 0x09, 2, min(max(world.options.exp_modifier.value, 0), 2**16 - 1))
_set_bytes_le(patched_rom, options_address + 0x0B, 2, 100)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x09,
struct.pack("<H", min(max(world.options.exp_modifier.value, 0), 2**16 - 1))
)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x0B,
struct.pack("<H", 100)
)
# Set match trainer levels
_set_bytes_le(patched_rom, options_address + 0x0D, 1, 1 if world.options.match_trainer_levels else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x0D,
struct.pack("<B", 1 if world.options.match_trainer_levels else 0)
)
# Set match trainer levels bonus
if world.options.match_trainer_levels == MatchTrainerLevels.option_additive:
match_trainer_levels_bonus = max(min(world.options.match_trainer_levels_bonus.value, 100), -100)
_set_bytes_le(patched_rom, options_address + 0x0E, 1, match_trainer_levels_bonus) # Works with negatives
patch.write_token(APTokenTypes.WRITE, options_address + 0x0E, struct.pack("<b", match_trainer_levels_bonus))
elif world.options.match_trainer_levels == MatchTrainerLevels.option_multiplicative:
_set_bytes_le(patched_rom, options_address + 0x2E, 2, world.options.match_trainer_levels_bonus.value + 100)
_set_bytes_le(patched_rom, options_address + 0x30, 2, 100)
patch.write_token(APTokenTypes.WRITE, options_address + 0x2E, struct.pack("<H", world.options.match_trainer_levels_bonus.value + 100))
patch.write_token(APTokenTypes.WRITE, options_address + 0x30, struct.pack("<H", 100))
# Set elite four requirement
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x0F,
1,
1 if world.options.elite_four_requirement == EliteFourRequirement.option_gyms else 0
struct.pack("<B", 1 if world.options.elite_four_requirement == EliteFourRequirement.option_gyms else 0)
)
# Set elite four count
_set_bytes_le(patched_rom, options_address + 0x10, 1, min(max(world.options.elite_four_count.value, 0), 8))
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x10,
struct.pack("<B", min(max(world.options.elite_four_count.value, 0), 8))
)
# Set norman requirement
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x11,
1,
1 if world.options.norman_requirement == NormanRequirement.option_gyms else 0
struct.pack("<B", 1 if world.options.norman_requirement == NormanRequirement.option_gyms else 0)
)
# Set norman count
_set_bytes_le(patched_rom, options_address + 0x12, 1, min(max(world.options.norman_count.value, 0), 8))
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x12,
struct.pack("<B", min(max(world.options.norman_count.value, 0), 8))
)
# Set starting badges
_set_bytes_le(patched_rom, options_address + 0x13, 1, starting_badges)
patch.write_token(APTokenTypes.WRITE, options_address + 0x13, struct.pack("<B", starting_badges))
# Set HM badge requirements
field_move_order = [
@@ -455,7 +519,7 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
hm_badge_counts = 0
for i, hm in enumerate(field_move_order):
hm_badge_counts |= (world.hm_requirements[hm] if isinstance(world.hm_requirements[hm], int) else 0xF) << (i * 4)
_set_bytes_le(patched_rom, options_address + 0x14, 4, hm_badge_counts)
patch.write_token(APTokenTypes.WRITE, options_address + 0x14, struct.pack("<I", hm_badge_counts))
# Specific badges
for i, hm in enumerate(field_move_order):
@@ -463,21 +527,37 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
bitfield = 0
for badge in world.hm_requirements:
bitfield |= badge_to_bit[badge]
_set_bytes_le(patched_rom, options_address + 0x18 + i, 1, bitfield)
patch.write_token(APTokenTypes.WRITE, options_address + 0x18, struct.pack("<B", bitfield))
# Set terra/marine cave locations
terra_cave_id = CAVE_EVENT_NAME_TO_ID[world.multiworld.get_location("TERRA_CAVE_LOCATION", world.player).item.name]
marine_cave_id = CAVE_EVENT_NAME_TO_ID[world.multiworld.get_location("MARINE_CAVE_LOCATION", world.player).item.name]
_set_bytes_le(patched_rom, options_address + 0x21, 1, terra_cave_id | (marine_cave_id << 4))
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x21,
struct.pack("<B", terra_cave_id | (marine_cave_id << 4))
)
# Set route 115 boulders
_set_bytes_le(patched_rom, options_address + 0x22, 1, 1 if world.options.extra_boulders else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x22,
struct.pack("<B", 1 if world.options.extra_boulders else 0)
)
# Swap route 115 layout if bumpy slope enabled
_set_bytes_le(patched_rom, options_address + 0x23, 1, 1 if world.options.extra_bumpy_slope else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x23,
struct.pack("<B", 1 if world.options.extra_bumpy_slope else 0)
)
# Swap route 115 layout if bumpy slope enabled
_set_bytes_le(patched_rom, options_address + 0x24, 1, 1 if world.options.modify_118 else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x24,
struct.pack("<B", 1 if world.options.modify_118 else 0)
)
# Set removed blockers
removed_roadblocks = world.options.remove_roadblocks.value
@@ -489,44 +569,72 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
removed_roadblocks_bitfield |= (1 << 4) if "Route 119 Aqua Grunts" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 5) if "Route 112 Magma Grunts" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 6) if "Seafloor Cavern Aqua Grunt" in removed_roadblocks else 0
_set_bytes_le(patched_rom, options_address + 0x25, 2, removed_roadblocks_bitfield)
patch.write_token(APTokenTypes.WRITE, options_address + 0x25, struct.pack("<H", removed_roadblocks_bitfield))
# Mark berry trees as randomized
_set_bytes_le(patched_rom, options_address + 0x27, 1, 1 if world.options.berry_trees else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x27,
struct.pack("<B", 1 if world.options.berry_trees else 0)
)
# Mark dexsanity as enabled
_set_bytes_le(patched_rom, options_address + 0x28, 1, 1 if world.options.dexsanity else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x28,
struct.pack("<B", 1 if world.options.dexsanity else 0)
)
# Mark trainersanity as enabled
_set_bytes_le(patched_rom, options_address + 0x29, 1, 1 if world.options.trainersanity else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x29,
struct.pack("<B", 1 if world.options.trainersanity else 0)
)
# Set easter egg data
_set_bytes_le(patched_rom, options_address + 0x2B, 1, easter_egg[0])
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x2B,
struct.pack("<B", easter_egg[0])
)
# Set normalize encounter rates
_set_bytes_le(patched_rom, options_address + 0x2C, 1, 1 if world.options.normalize_encounter_rates else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x2C,
struct.pack("<B", 1 if world.options.normalize_encounter_rates else 0)
)
# Set allow wonder trading
_set_bytes_le(patched_rom, options_address + 0x2D, 1, 1 if world.options.enable_wonder_trading else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x2D,
struct.pack("<B", 1 if world.options.enable_wonder_trading else 0)
)
# Set allowed to skip fanfares
_set_bytes_le(patched_rom, options_address + 0x32, 1, 1 if world.options.fanfares else 0)
patch.write_token(
APTokenTypes.WRITE,
options_address + 0x32,
struct.pack("<B", 1 if world.options.fanfares else 0)
)
if easter_egg[0] == 2:
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (easter_egg[1] * 12) + 4, 1, 50)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_CUT"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_FLY"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_SURF"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_STRENGTH"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_FLASH"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_ROCK_SMASH"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_WATERFALL"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_DIVE"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_DIG"] * 12) + 4, 1, 1)
offset = data.rom_addresses["gBattleMoves"] + 4
patch.write_token(APTokenTypes.WRITE, offset + (easter_egg[1] * 12), struct.pack("<B", 50))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_CUT"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_FLY"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_SURF"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_STRENGTH"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_FLASH"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_ROCK_SMASH"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_WATERFALL"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_DIVE"] * 12), struct.pack("<B", 1))
patch.write_token(APTokenTypes.WRITE, offset + (data.constants["MOVE_DIG"] * 12), struct.pack("<B", 1))
# Set slot auth
for i, byte in enumerate(world.auth):
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoInfo"] + i, 1, byte)
patch.write_token(APTokenTypes.WRITE, data.rom_addresses["gArchipelagoInfo"], world.auth)
# Randomize music
if world.options.music:
@@ -534,11 +642,10 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
randomized_looping_music = copy.copy(_LOOPING_MUSIC)
world.random.shuffle(randomized_looping_music)
for original_music, randomized_music in zip(_LOOPING_MUSIC, randomized_looping_music):
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gRandomizedSoundTable"] + (data.constants[original_music] * 2),
2,
data.constants[randomized_music]
struct.pack("<H", data.constants[randomized_music])
)
# Randomize fanfares
@@ -547,40 +654,21 @@ def create_patch(world: "PokemonEmeraldWorld", output_directory: str) -> None:
randomized_fanfares = [fanfare_name for fanfare_name in _FANFARES]
world.random.shuffle(randomized_fanfares)
for i, fanfare_pair in enumerate(zip(_FANFARES.keys(), randomized_fanfares)):
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gRandomizedSoundTable"] + (data.constants[fanfare_pair[0]] * 2),
2,
data.constants[fanfare_pair[1]]
struct.pack("<H", data.constants[fanfare_pair[1]])
)
_set_bytes_le(
patched_rom,
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["sFanfares"] + (i * 4) + 2,
2,
_FANFARES[fanfare_pair[1]]
struct.pack("<H", _FANFARES[fanfare_pair[1]])
)
# Write Output
out_file_name = world.multiworld.get_out_file_name_base(world.player)
output_path = os.path.join(output_directory, f"{out_file_name}.gba")
with open(output_path, "wb") as out_file:
out_file.write(patched_rom)
patch = PokemonEmeraldDeltaPatch(os.path.splitext(output_path)[0] + ".apemerald", player=world.player,
player_name=world.multiworld.get_player_name(world.player),
patched_path=output_path)
patch.write()
os.unlink(output_path)
patch.write_file("token_data.bin", patch.get_token_binary())
def get_base_rom_as_bytes() -> bytes:
with open(get_settings().pokemon_emerald_settings.rom_file, "rb") as infile:
base_rom_bytes = bytes(infile.read())
return base_rom_bytes
def _set_encounter_tables(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _set_encounter_tables(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
"""
Encounter tables are lists of
struct {
@@ -595,30 +683,31 @@ def _set_encounter_tables(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
if table is not None:
for i, species_id in enumerate(table.slots):
address = table.address + 2 + (4 * i)
_set_bytes_le(rom, address, 2, species_id)
patch.write_token(APTokenTypes.WRITE, address, struct.pack("<H", species_id))
def _set_species_info(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
def _set_species_info(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None:
for species in world.modified_species.values():
_set_bytes_le(rom, species.address + 6, 1, species.types[0])
_set_bytes_le(rom, species.address + 7, 1, species.types[1])
_set_bytes_le(rom, species.address + 8, 1, species.catch_rate)
_set_bytes_le(rom, species.address + 22, 1, species.abilities[0])
_set_bytes_le(rom, species.address + 23, 1, species.abilities[1])
patch.write_token(APTokenTypes.WRITE, species.address + 6, struct.pack("<B", species.types[0]))
patch.write_token(APTokenTypes.WRITE, species.address + 7, struct.pack("<B", species.types[1]))
patch.write_token(APTokenTypes.WRITE, species.address + 8, struct.pack("<B", species.catch_rate))
if easter_egg[0] == 3:
_set_bytes_le(rom, species.address + 22, 1, easter_egg[1])
_set_bytes_le(rom, species.address + 23, 1, easter_egg[1])
patch.write_token(APTokenTypes.WRITE, species.address + 22, struct.pack("<B", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, species.address + 23, struct.pack("<B", easter_egg[1]))
else:
patch.write_token(APTokenTypes.WRITE, species.address + 22, struct.pack("<B", species.abilities[0]))
patch.write_token(APTokenTypes.WRITE, species.address + 23, struct.pack("<B", species.abilities[1]))
for i, learnset_move in enumerate(species.learnset):
level_move = learnset_move.level << 9 | learnset_move.move_id
if easter_egg[0] == 2:
level_move = learnset_move.level << 9 | easter_egg[1]
_set_bytes_le(rom, species.learnset_address + (i * 2), 2, level_move)
patch.write_token(APTokenTypes.WRITE, species.learnset_address + (i * 2), struct.pack("<H", level_move))
def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
def _set_opponents(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None:
for trainer in world.modified_trainers:
party_address = trainer.party.address
@@ -632,53 +721,50 @@ def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tup
pokemon_address = party_address + (i * pokemon_data_size)
# Replace species
_set_bytes_le(rom, pokemon_address + 0x04, 2, pokemon.species_id)
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x04, struct.pack("<H", pokemon.species_id))
# Replace custom moves if applicable
if trainer.party.pokemon_data_type == TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES:
if easter_egg[0] == 2:
_set_bytes_le(rom, pokemon_address + 0x06, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x08, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, easter_egg[1])
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x06, struct.pack("<H", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x08, struct.pack("<H", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0A, struct.pack("<H", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0C, struct.pack("<H", easter_egg[1]))
else:
_set_bytes_le(rom, pokemon_address + 0x06, 2, pokemon.moves[0])
_set_bytes_le(rom, pokemon_address + 0x08, 2, pokemon.moves[1])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, pokemon.moves[2])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, pokemon.moves[3])
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x06, struct.pack("<H", pokemon.moves[0]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x08, struct.pack("<H", pokemon.moves[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0A, struct.pack("<H", pokemon.moves[2]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0C, struct.pack("<H", pokemon.moves[3]))
elif trainer.party.pokemon_data_type == TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES:
if easter_egg[0] == 2:
_set_bytes_le(rom, pokemon_address + 0x08, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0E, 2, easter_egg[1])
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x08, struct.pack("<H", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0A, struct.pack("<H", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0C, struct.pack("<H", easter_egg[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0E, struct.pack("<H", easter_egg[1]))
else:
_set_bytes_le(rom, pokemon_address + 0x08, 2, pokemon.moves[0])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, pokemon.moves[1])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, pokemon.moves[2])
_set_bytes_le(rom, pokemon_address + 0x0E, 2, pokemon.moves[3])
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x08, struct.pack("<H", pokemon.moves[0]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0A, struct.pack("<H", pokemon.moves[1]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0C, struct.pack("<H", pokemon.moves[2]))
patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x0E, struct.pack("<H", pokemon.moves[3]))
def _set_legendary_encounters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _set_legendary_encounters(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
for encounter in world.modified_legendary_encounters:
_set_bytes_le(rom, encounter.address, 2, encounter.species_id)
patch.write_token(APTokenTypes.WRITE, encounter.address, struct.pack("<H", encounter.species_id))
def _set_misc_pokemon(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _set_misc_pokemon(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
for encounter in world.modified_misc_pokemon:
_set_bytes_le(rom, encounter.address, 2, encounter.species_id)
patch.write_token(APTokenTypes.WRITE, encounter.address, struct.pack("<H", encounter.species_id))
def _set_starters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
address = data.rom_addresses["sStarterMon"]
(starter_1, starter_2, starter_3) = world.modified_starters
_set_bytes_le(rom, address + 0, 2, starter_1)
_set_bytes_le(rom, address + 2, 2, starter_2)
_set_bytes_le(rom, address + 4, 2, starter_3)
def _set_starters(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
patch.write_token(APTokenTypes.WRITE, data.rom_addresses["sStarterMon"] + 0, struct.pack("<H", world.modified_starters[0]))
patch.write_token(APTokenTypes.WRITE, data.rom_addresses["sStarterMon"] + 2, struct.pack("<H", world.modified_starters[1]))
patch.write_token(APTokenTypes.WRITE, data.rom_addresses["sStarterMon"] + 4, struct.pack("<H", world.modified_starters[2]))
def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
def _set_tm_moves(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None:
tmhm_list_address = data.rom_addresses["sTMHMMoves"]
for i, move in enumerate(world.modified_tmhm_moves):
@@ -686,19 +772,24 @@ def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tupl
if i >= 50:
break
_set_bytes_le(rom, tmhm_list_address + (i * 2), 2, move)
if easter_egg[0] == 2:
_set_bytes_le(rom, tmhm_list_address + (i * 2), 2, easter_egg[1])
patch.write_token(APTokenTypes.WRITE, tmhm_list_address + (i * 2), struct.pack("<H", easter_egg[1]))
else:
patch.write_token(APTokenTypes.WRITE, tmhm_list_address + (i * 2), struct.pack("<H", move))
def _set_tmhm_compatibility(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _set_tmhm_compatibility(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
learnsets_address = data.rom_addresses["gTMHMLearnsets"]
for species in world.modified_species.values():
_set_bytes_le(rom, learnsets_address + (species.species_id * 8), 8, species.tm_hm_compatibility)
patch.write_token(
APTokenTypes.WRITE,
learnsets_address + (species.species_id * 8),
struct.pack("<Q", species.tm_hm_compatibility)
)
def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
probability = world.options.double_battle_chance.value / 100
battle_type_map = {
@@ -710,26 +801,29 @@ def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", rom: bytearray
for trainer_data in data.trainers:
if trainer_data.script_address != 0 and len(trainer_data.party.pokemon) > 1:
original_battle_type = rom[trainer_data.script_address + 1]
original_battle_type = trainer_data.battle_type
if original_battle_type in battle_type_map: # Don't touch anything other than regular single battles
if world.random.random() < probability:
# Set the trainer to be a double battle
_set_bytes_le(rom, trainer_data.address + 0x18, 1, 1)
patch.write_token(APTokenTypes.WRITE, trainer_data.address + 0x18, struct.pack("<B", 1))
# Swap the battle type in the script for the purpose of loading the right text
# and setting data to the right places
_set_bytes_le(
rom,
patch.write_token(
APTokenTypes.WRITE,
trainer_data.script_address + 1,
1,
battle_type_map[original_battle_type]
struct.pack("<B", battle_type_map[original_battle_type])
)
def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None:
if easter_egg[0] == 2:
for i in range(30):
_set_bytes_le(rom, data.rom_addresses["gTutorMoves"] + (i * 2), 2, easter_egg[1])
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gTutorMoves"] + (i * 2),
struct.pack("<H", easter_egg[1])
)
else:
if world.options.tm_tutor_moves:
new_tutor_moves = []
@@ -737,17 +831,27 @@ def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", rom: bytearray, ea
new_move = get_random_move(world.random, set(new_tutor_moves) | world.blacklisted_moves | HM_MOVES)
new_tutor_moves.append(new_move)
_set_bytes_le(rom, data.rom_addresses["gTutorMoves"] + (i * 2), 2, new_move)
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gTutorMoves"] + (i * 2),
struct.pack("<H", new_move)
)
# Always set Fortree move tutor to Dig
_set_bytes_le(rom, data.rom_addresses["gTutorMoves"] + (24 * 2), 2, data.constants["MOVE_DIG"])
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["gTutorMoves"] + (24 * 2),
struct.pack("<H", data.constants["MOVE_DIG"])
)
# Modify compatibility
if world.options.tm_tutor_compatibility.value != -1:
for species in data.species.values():
_set_bytes_le(
rom,
patch.write_token(
APTokenTypes.WRITE,
data.rom_addresses["sTutorLearnsets"] + (species.species_id * 4),
4,
bool_array_to_int([world.random.randrange(0, 100) < world.options.tm_tutor_compatibility.value for _ in range(32)])
struct.pack("<I", bool_array_to_int([
world.random.randrange(0, 100) < world.options.tm_tutor_compatibility.value
for _ in range(32)
]))
)

View File

@@ -464,7 +464,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Slateport City
set_rule(
get_entrance("REGION_SLATEPORT_CITY/MAIN -> REGION_ROUTE134/WEST"),
get_entrance("REGION_SLATEPORT_CITY/MAIN -> REGION_SLATEPORT_CITY/WATER"),
hm_rules["HM03 Surf"]
)
set_rule(
@@ -1531,6 +1531,10 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
if world.options.dexsanity:
for i in range(NUM_REAL_SPECIES):
species = data.species[NATIONAL_ID_TO_SPECIES_ID[i + 1]]
if species.species_id in world.blacklisted_wilds:
continue
set_rule(
get_location(f"Pokedex - {species.label}"),
lambda state, species_name=species.name: state.has(f"CATCH_{species_name}", world.player)
@@ -1538,7 +1542,8 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Legendary hunt prevents Latios from being a wild spawn so the roamer
# can be tracked, and also guarantees that the roamer is a Latios.
if world.options.goal == Goal.option_legendary_hunt:
if world.options.goal == Goal.option_legendary_hunt and \
data.constants["SPECIES_LATIOS"] not in world.blacklisted_wilds:
set_rule(
get_location(f"Pokedex - Latios"),
lambda state: state.has("EVENT_ENCOUNTER_LATIOS", world.player)

View File

@@ -59,6 +59,10 @@ class TestSurf(PokemonEmeraldTestBase):
self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
# Slateport Access
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Mach Bike"])
self.assertFalse(self.can_reach_region("MAP_SLATEPORT_CITY_WATER_ENCOUNTERS"))
def test_accessible_with_surf_only(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge"])
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
@@ -70,6 +74,7 @@ class TestSurf(PokemonEmeraldTestBase):
self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_4")))
self.assertTrue(self.can_reach_region("MAP_SLATEPORT_CITY_WATER_ENCOUNTERS"))
class TestFreeFly(PokemonEmeraldTestBase):

View File

@@ -650,7 +650,7 @@ campaign_final_mission_locations: Dict[SC2Campaign, SC2CampaignGoal] = {
SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, "Evil Awoken: Victory"),
SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, "Salvation: Victory"),
SC2Campaign.EPILOGUE: None,
SC2Campaign.NCO: None,
SC2Campaign.NCO: SC2CampaignGoal(SC2Mission.END_GAME, "End Game: Victory"),
}
campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = {
@@ -683,7 +683,6 @@ campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] =
SC2Mission.THE_ESSENCE_OF_ETERNITY: "The Essence of Eternity: Victory",
},
SC2Campaign.NCO: {
SC2Mission.END_GAME: "End Game: Victory",
SC2Mission.FLASHPOINT: "Flashpoint: Victory",
SC2Mission.DARK_SKIES: "Dark Skies: Victory",
SC2Mission.NIGHT_TERRORS: "Night Terrors: Victory",
@@ -709,10 +708,10 @@ def get_goal_location(mission: SC2Mission) -> Union[str, None]:
return primary_campaign_goal.location
campaign_alt_goals = campaign_alt_final_mission_locations[campaign]
if campaign_alt_goals is not None:
if campaign_alt_goals is not None and mission in campaign_alt_goals:
return campaign_alt_goals.get(mission)
return None
return mission.mission_name + ": Victory"
def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Mission]:

View File

@@ -1,4 +1,4 @@
from typing import Callable, Dict, List, Set, Union, Tuple
from typing import Callable, Dict, List, Set, Union, Tuple, Optional
from BaseClasses import Item, Location
from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, progressive_if_nco, \
progressive_if_ext, spear_of_adun_calldowns, spear_of_adun_castable_passives, nova_equipment
@@ -69,21 +69,39 @@ def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]:
return mission_pools
# Finding the goal map
goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
goal_campaign = world.random.choice(candidate_campaigns)
primary_goal = campaign_final_mission_locations[goal_campaign]
if primary_goal is None or primary_goal.mission in excluded_missions:
# No primary goal or its mission is excluded
candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys())
candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions]
if len(candidate_missions) == 0:
raise Exception("There are no valid goal missions. Please exclude fewer missions.")
goal_mission = world.random.choice(candidate_missions)
goal_mission: Optional[SC2Mission] = None
if mission_order_type in campaign_depending_orders:
# Prefer long campaigns over shorter ones and harder missions over easier ones
goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
goal_campaign = world.random.choice(candidate_campaigns)
primary_goal = campaign_final_mission_locations[goal_campaign]
if primary_goal is None or primary_goal.mission in excluded_missions:
# No primary goal or its mission is excluded
candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys())
candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions]
if len(candidate_missions) == 0:
raise Exception("There are no valid goal missions. Please exclude fewer missions.")
goal_mission = world.random.choice(candidate_missions)
else:
goal_mission = primary_goal.mission
else:
goal_mission = primary_goal.mission
# Find one of the missions with the hardest difficulty
available_missions: List[SC2Mission] = \
[mission for mission in SC2Mission
if (mission not in excluded_missions and mission.campaign in enabled_campaigns)]
available_missions.sort(key=lambda it: it.id)
# Loop over pools, from hardest to easiest
for mission_pool in range(MissionPools.VERY_HARD, MissionPools.STARTER - 1, -1):
pool_missions: List[SC2Mission] = [mission for mission in available_missions if mission.pool == mission_pool]
if pool_missions:
goal_mission = world.random.choice(pool_missions)
break
if goal_mission is None:
raise Exception("There are no valid goal missions. Please exclude fewer missions.")
# Excluding missions
for difficulty, mission_pool in mission_pools.items():

View File

@@ -6,23 +6,23 @@ The player options page for this game contains all the options you need to confi
options page link: [SM64EX Player Options Page](../player-options).
## What does randomization do to this game?
All 120 Stars, the 3 Cap Switches, the Basement and Secound Floor Key are now Location Checks and may contain Items for different games as well
as different Items from within SM64.
All 120 Stars, the 3 Cap Switches, the Basement and Second Floor Key are now location checks and may contain items for different games as well
as different items from within SM64.
## What is the goal of SM64EX when randomized?
As in most Mario Games, save the Princess!
As in most Mario games, save the Princess!
## Which items can be in another player's world?
Any of the 120 Stars, and the two Castle Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
when someone collects the corresponding Cap Switch Item.
when someone collects the corresponding Cap Switch item.
## What does another world's item look like in SM64EX?
The Items are visually unchanged, though after collecting a Message will pop up to inform you what you collected,
The items are visually unchanged, though after collecting a message will pop up to inform you what you collected,
and who will receive it.
## When the player receives an item, what happens?
When you receive an Item, a Message will pop up to inform you where you received the Item from,
When you receive an item, a message will pop up to inform you where you received the item from,
and which one it is.
NOTE: The Secret Star count in the Menu is broken.
NOTE: The Secret Star count in the menu is broken.

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) (optional, but recommended).
- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
- SNI is not compatible with (Q)Usb2Snes.
- Hardware or software capable of loading and playing SNES ROM files, including:
@@ -14,6 +14,7 @@
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:
modded SNES minis are currently not supported by SNI. Some users have claimed success with QUsb2Snes for this system,
but it is not supported.**
- A modern web browser to run the client.
- Your legally obtained Secret of Evermore US ROM file, probably named `Secret of Evermore (USA).sfc`
## Create a Config (.yaml) File
@@ -60,12 +61,12 @@ page: [Evermizer apbpatch Page](https://evermizer.com/apbpatch)
### Connect to SNI
#### With an emulator
Start SNI either from the Archipelago install folder or the stand-alone version. If this is its first time launching,
you may be prompted to allow it to communicate through the Windows Firewall.
#### snes9x-nwa
#### With an emulator
##### snes9x-nwa
1. Click on the Network Menu and check **Enable Emu Network Control**
2. Load your ROM file if it hasn't already been loaded.

View File

@@ -24,7 +24,7 @@ item_table: Dict[str, ItemData] = {
"Red Candle": ItemData(107, progression),
"Book of Magic": ItemData(108, progression),
"Magical Key": ItemData(109, useful),
"Red Ring": ItemData(110, useful),
"Red Ring": ItemData(110, progression),
"Silver Arrow": ItemData(111, progression),
"Sword": ItemData(112, progression),
"White Sword": ItemData(113, progression),
@@ -37,7 +37,7 @@ item_table: Dict[str, ItemData] = {
"Food": ItemData(120, progression),
"Water of Life (Blue)": ItemData(121, useful),
"Water of Life (Red)": ItemData(122, useful),
"Blue Ring": ItemData(123, useful),
"Blue Ring": ItemData(123, progression),
"Triforce Fragment": ItemData(124, progression),
"Power Bracelet": ItemData(125, useful),
"Small Key": ItemData(126, filler),

View File

@@ -28,6 +28,7 @@ def set_rules(tloz_world: "TLoZWorld"):
or location.name not in dangerous_weapon_locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("weapons", player))
# This part of the loop sets up an expected amount of defense needed for each dungeon
if i > 0: # Don't need an extra heart for Level 1
add_rule(world.get_location(location.name, player),
lambda state, hearts=i: state.has("Heart Container", player, hearts) or
@@ -49,7 +50,7 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in level.locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("candles", player)
or (state.has("Magical Rod", player) and state.has("Book", player)))
or (state.has("Magical Rod", player) and state.has("Book of Magic", player)))
# Everything from 5 on up has gaps
for level in tloz_world.levels[5:]:
@@ -84,6 +85,11 @@ def set_rules(tloz_world: "TLoZWorld"):
add_rule(world.get_location(location, player),
lambda state: state.has_group("swords", player) or state.has("Magical Rod", player))
# Candle access for Level 8
for location in tloz_world.levels[8].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("candles", player))
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
if options.ExpandedPool:

View File

@@ -260,11 +260,11 @@ class TLoZWorld(World):
rom_data[location_id] = item_id
# We shuffle the tiers of rupee caves. Caves that shared a value before still will.
secret_caves = self.multiworld.per_slot_randoms[self.player].sample(sorted(secret_money_ids), 3)
secret_caves = self.random.sample(sorted(secret_money_ids), 3)
secret_cave_money_amounts = [20, 50, 100]
for i, amount in enumerate(secret_cave_money_amounts):
# Giving approximately double the money to keep grinding down
amount = amount * self.multiworld.per_slot_randoms[self.player].triangular(1.5, 2.5)
amount = amount * self.random.triangular(1.5, 2.5)
secret_cave_money_amounts[i] = int(amount)
for i, cave in enumerate(secret_caves):
rom_data[secret_money_ids[cave]] = secret_cave_money_amounts[i]

View File

@@ -1,6 +1,6 @@
from typing import Dict, List, Any
from typing import Dict, List, Any, Tuple, TypedDict
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
@@ -8,8 +8,9 @@ from .er_rules import set_er_location_rules
from .regions import tunic_regions
from .er_scripts import create_er_regions
from .er_data import portal_mapping
from .options import TunicOptions
from .options import TunicOptions, EntranceRando
from worlds.AutoWorld import WebWorld, World
from worlds.generic import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP
@@ -36,6 +37,13 @@ class TunicLocation(Location):
game: str = "TUNIC"
class SeedGroup(TypedDict):
logic_rules: int # logic rules value
laurels_at_10_fairies: bool # laurels location value
fixed_shop: bool # fixed shop value
plando: List[PlandoConnection] # consolidated list of plando connections for the seed group
class TunicWorld(World):
"""
Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game
@@ -57,8 +65,21 @@ class TunicWorld(World):
slot_data_items: List[TunicItem]
tunic_portal_pairs: Dict[str, str]
er_portal_hints: Dict[int, str]
seed_groups: Dict[str, SeedGroup] = {}
def generate_early(self) -> None:
if self.multiworld.plando_connections[self.player]:
for index, cxn in enumerate(self.multiworld.plando_connections[self.player]):
# making shops second to simplify other things later
if cxn.entrance.startswith("Shop"):
replacement = PlandoConnection(cxn.exit, "Shop Portal", "both")
self.multiworld.plando_connections[self.player].remove(cxn)
self.multiworld.plando_connections[self.player].insert(index, replacement)
elif cxn.exit.startswith("Shop"):
replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both")
self.multiworld.plando_connections[self.player].remove(cxn)
self.multiworld.plando_connections[self.player].insert(index, replacement)
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
@@ -74,6 +95,58 @@ class TunicWorld(World):
self.options.entrance_rando.value = passthrough["entrance_rando"]
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
for tunic in tunic_worlds:
# if it's one of the options, then it isn't a custom seed group
if tunic.options.entrance_rando.value in EntranceRando.options:
continue
group = tunic.options.entrance_rando.value
# if this is the first world in the group, set the rules equal to its rules
if group not in cls.seed_groups:
cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value,
laurels_at_10_fairies=tunic.options.laurels_location == 3,
fixed_shop=bool(tunic.options.fixed_shop),
plando=multiworld.plando_connections[tunic.player])
continue
# lower value is more restrictive
if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]:
cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value
# laurels at 10 fairies changes logic for secret gathering place placement
if tunic.options.laurels_location == 3:
cls.seed_groups[group]["laurels_at_10_fairies"] = True
# fewer shops, one at windmill
if tunic.options.fixed_shop:
cls.seed_groups[group]["fixed_shop"] = True
if multiworld.plando_connections[tunic.player]:
# loop through the connections in the player's yaml
for cxn in multiworld.plando_connections[tunic.player]:
new_cxn = True
for group_cxn in cls.seed_groups[group]["plando"]:
# if neither entrance nor exit match anything in the group, add to group
if ((cxn.entrance == group_cxn.entrance and cxn.exit == group_cxn.exit)
or (cxn.exit == group_cxn.entrance and cxn.entrance == group_cxn.exit)):
new_cxn = False
break
# check if this pair is the same as a pair in the group already
is_mismatched = (
cxn.entrance == group_cxn.entrance and cxn.exit != group_cxn.exit
or cxn.entrance == group_cxn.exit and cxn.exit != group_cxn.entrance
or cxn.exit == group_cxn.entrance and cxn.entrance != group_cxn.exit
or cxn.exit == group_cxn.exit and cxn.entrance != group_cxn.entrance
)
if is_mismatched:
raise Exception(f"TUNIC: Conflict between seed group {group}'s plando "
f"connection {group_cxn.entrance} <-> {group_cxn.exit} and "
f"{tunic.multiworld.get_player_name(tunic.player)}'s plando "
f"connection {cxn.entrance} <-> {cxn.exit}")
if new_cxn:
cls.seed_groups[group]["plando"].append(cxn)
def create_item(self, name: str) -> TunicItem:
item_data = item_table[name]
return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player)
@@ -140,7 +213,7 @@ class TunicWorld(World):
if self.options.shuffle_ladders:
ladder_count = 0
for item_name, item_data in item_table.items():
if item_data.item_group == "ladders":
if item_data.item_group == "Ladders":
items_to_create[item_name] = 1
ladder_count += 1
remove_filler(ladder_count)
@@ -259,7 +332,7 @@ class TunicWorld(World):
name, connection = connection
# for LS entrances, we just want to give the portal name
if "(LS)" in name:
name, _ = name.split(" (LS) ")
name = name.split(" (LS) ", 1)[0]
# was getting some cases like Library Grave -> Library Grave -> other place
if name in portal_names and name != previous_name:
previous_name = name

View File

@@ -64,11 +64,8 @@ For the Entrance Randomizer:
- The portal in the trophy room of the Old House is active from the start.
- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing.
## What item groups are there?
Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, icebolt, and progressive sword.
## What location groups are there?
Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), shop, bosses (for the bosses with checks associated with them), hero relic (for the 6 hero grave checks), and ladders (for the ladder items when you have shuffle ladders enabled).
## Does this game have item and location groups?
Yes! To find what they are, open up the Archipelago Text Client while connected to a TUNIC session and type in `/item_groups` or `/location_groups`.
## Is Connection Plando supported?
Yes. The host needs to enable it in their `host.yaml`, and the player's yaml needs to contain a plando_connections block.

View File

@@ -1452,7 +1452,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int])
# Beneath the Vault
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
lambda state: state.has_group("melee weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player),
lambda state: has_lantern(state, player, options))

View File

@@ -4,6 +4,7 @@ from .locations import location_table
from .er_data import Portal, tunic_er_regions, portal_mapping, \
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
from .er_rules import set_er_region_rules
from .options import EntranceRando
from worlds.generic import PlandoConnection
from random import Random
@@ -128,12 +129,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
portal_pairs: Dict[Portal, Portal] = {}
dead_ends: List[Portal] = []
two_plus: List[Portal] = []
logic_rules = world.options.logic_rules.value
player_name = world.multiworld.get_player_name(world.player)
logic_rules = world.options.logic_rules.value
fixed_shop = world.options.fixed_shop
laurels_location = world.options.laurels_location
# if it's not one of the EntranceRando options, it's a custom seed
if world.options.entrance_rando.value not in EntranceRando.options:
seed_group = world.seed_groups[world.options.entrance_rando.value]
logic_rules = seed_group["logic_rules"]
fixed_shop = seed_group["fixed_shop"]
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
shop_scenes: Set[str] = set()
shop_count = 6
if world.options.fixed_shop.value:
if fixed_shop:
shop_count = 1
shop_scenes.add("Overworld Redux")
@@ -163,7 +173,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
start_region = "Overworld"
connected_regions.update(add_dependent_regions(start_region, logic_rules))
plando_connections = world.multiworld.plando_connections[world.player]
if world.options.entrance_rando.value in EntranceRando.options:
plando_connections = world.multiworld.plando_connections[world.player]
else:
plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"]
# universal tracker support stuff, don't need to care about region dependency
if hasattr(world.multiworld, "re_gen_passthrough"):
@@ -198,10 +211,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
p_entrance = connection.entrance
p_exit = connection.exit
if p_entrance.startswith("Shop"):
p_entrance = p_exit
p_exit = "Shop Portal"
portal1 = None
portal2 = None
@@ -213,7 +222,18 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
portal2 = portal
# search dead_ends individually since we can't really remove items from two_plus during the loop
if not portal1:
if portal1:
two_plus.remove(portal1)
else:
# if not both, they're both dead ends
if not portal2:
if world.options.entrance_rando.value not in EntranceRando.options:
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
"end to a dead end in their plando connections.")
else:
raise Exception(f"{player_name} paired a dead end to a dead end in their "
"plando connections.")
for portal in dead_ends:
if p_entrance == portal.name:
portal1 = portal
@@ -222,16 +242,18 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
raise Exception(f"Could not find entrance named {p_entrance} for "
f"plando connections in {player_name}'s YAML.")
dead_ends.remove(portal1)
else:
two_plus.remove(portal1)
if not portal2:
if portal2:
two_plus.remove(portal2)
else:
# check if portal2 is a dead end
for portal in dead_ends:
if p_exit == portal.name:
portal2 = portal
break
if p_exit in ["Shop Portal", "Shop"]:
portal2 = Portal(name="Shop Portal", region=f"Shop",
# if it's not a dead end, it might be a shop
if p_exit == "Shop Portal":
portal2 = Portal(name="Shop Portal", region="Shop",
destination="Previous Region", tag="_")
shop_count -= 1
if shop_count < 0:
@@ -240,13 +262,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
if p.name == p_entrance:
shop_scenes.add(p.scene())
break
# and if it's neither shop nor dead end, it just isn't correct
else:
if not portal2:
raise Exception(f"Could not find entrance named {p_exit} for "
f"plando connections in {player_name}'s YAML.")
dead_ends.remove(portal2)
else:
two_plus.remove(portal2)
portal_pairs[portal1] = portal2
@@ -270,7 +291,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# need to plando fairy cave, or it could end up laurels locked
# fix this later to be random after adding some item logic to dependent regions
if world.options.laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
portal2 = None
for portal in two_plus:
@@ -291,7 +312,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
two_plus.remove(portal1)
dead_ends.remove(portal2)
if world.options.fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Windmill_":
@@ -307,7 +328,8 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
two_plus.remove(portal1)
random_object: Random = world.random
if world.options.entrance_rando.value != 1:
# use the seed given in the options to shuffle the portals
if isinstance(world.options.entrance_rando.value, str):
random_object = Random(world.options.entrance_rando.value)
# we want to start by making sure every region is accessible
random_object.shuffle(two_plus)

View File

@@ -13,158 +13,158 @@ class TunicItemData(NamedTuple):
item_base_id = 509342400
item_table: Dict[str, TunicItemData] = {
"Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "bombs"),
"Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "bombs"),
"Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "bombs"),
"Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "bombs"),
"Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "bombs"),
"Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "bombs"),
"Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "bombs"),
"Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "bombs"),
"Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "bombs"),
"Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "bombs"),
"Lure": TunicItemData(ItemClassification.filler, 4, 10, "consumables"),
"Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "consumables"),
"Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "consumables"),
"Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "consumables"),
"Effigy": TunicItemData(ItemClassification.useful, 12, 14, "money"),
"HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "consumables"),
"HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "consumables"),
"HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "consumables"),
"MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "consumables"),
"MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "consumables"),
"MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "consumables"),
"Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "Bombs"),
"Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "Bombs"),
"Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "Bombs"),
"Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "Bombs"),
"Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "Bombs"),
"Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "Bombs"),
"Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "Bombs"),
"Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "Bombs"),
"Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "Bombs"),
"Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "Bombs"),
"Lure": TunicItemData(ItemClassification.filler, 4, 10, "Consumables"),
"Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "Consumables"),
"Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "Consumables"),
"Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "Consumables"),
"Effigy": TunicItemData(ItemClassification.useful, 12, 14, "Money"),
"HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "Consumables"),
"HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "Consumables"),
"HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "Consumables"),
"MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "Consumables"),
"MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "Consumables"),
"MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "Consumables"),
"Fairy": TunicItemData(ItemClassification.progression, 20, 21),
"Stick": TunicItemData(ItemClassification.progression, 1, 22, "weapons"),
"Sword": TunicItemData(ItemClassification.progression, 3, 23, "weapons"),
"Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "weapons"),
"Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "weapons"),
"Stick": TunicItemData(ItemClassification.progression, 1, 22, "Weapons"),
"Sword": TunicItemData(ItemClassification.progression, 3, 23, "Weapons"),
"Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "Weapons"),
"Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "Weapons"),
"Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26),
"Magic Orb": TunicItemData(ItemClassification.progression, 1, 27),
"Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28),
"Lantern": TunicItemData(ItemClassification.progression, 1, 29),
"Gun": TunicItemData(ItemClassification.useful, 1, 30, "weapons"),
"Gun": TunicItemData(ItemClassification.useful, 1, 30, "Weapons"),
"Shield": TunicItemData(ItemClassification.useful, 1, 31),
"Dath Stone": TunicItemData(ItemClassification.useful, 1, 32),
"Hourglass": TunicItemData(ItemClassification.useful, 1, 33),
"Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "keys"),
"Key": TunicItemData(ItemClassification.progression, 2, 35, "keys"),
"Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "keys"),
"Flask Shard": TunicItemData(ItemClassification.useful, 12, 37, "potions"),
"Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "potions"),
"Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "Keys"),
"Key": TunicItemData(ItemClassification.progression, 2, 35, "Keys"),
"Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "Keys"),
"Flask Shard": TunicItemData(ItemClassification.useful, 12, 37),
"Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "Flask"),
"Golden Coin": TunicItemData(ItemClassification.progression, 17, 39),
"Card Slot": TunicItemData(ItemClassification.useful, 4, 40),
"Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "hexagons"),
"Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "hexagons"),
"Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "hexagons"),
"Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "hexagons"),
"ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "offerings"),
"DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "offerings"),
"Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "offerings"),
"HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "offerings"),
"MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "offerings"),
"SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "offerings"),
"Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "hero relics"),
"Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "hero relics"),
"Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "hero relics"),
"Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "hero relics"),
"Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "hero relics"),
"Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "hero relics"),
"Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "cards"),
"Tincture": TunicItemData(ItemClassification.useful, 1, 58, "cards"),
"Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "cards"),
"Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "cards"),
"Bracer": TunicItemData(ItemClassification.useful, 1, 61, "cards"),
"Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "cards"),
"Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "cards"),
"Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "cards"),
"Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "cards"),
"Anklet": TunicItemData(ItemClassification.useful, 1, 66, "cards"),
"Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "cards"),
"Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "cards"),
"Perfume": TunicItemData(ItemClassification.useful, 1, 69, "cards"),
"Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "cards"),
"Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "cards"),
"Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "cards"),
"Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "golden treasures"),
"Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "golden treasures"),
"Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "golden treasures"),
"Vintage": TunicItemData(ItemClassification.useful, 1, 76, "golden treasures"),
"Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "golden treasures"),
"Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "golden treasures"),
"Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "golden treasures"),
"Power Up": TunicItemData(ItemClassification.useful, 1, 80, "golden treasures"),
"Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "golden treasures"),
"Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "golden treasures"),
"Dusty": TunicItemData(ItemClassification.useful, 1, 83, "golden treasures"),
"Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "golden treasures"),
"Fool Trap": TunicItemData(ItemClassification.trap, 0, 85, "fool"),
"Money x1": TunicItemData(ItemClassification.filler, 3, 86, "money"),
"Money x10": TunicItemData(ItemClassification.filler, 1, 87, "money"),
"Money x15": TunicItemData(ItemClassification.filler, 10, 88, "money"),
"Money x16": TunicItemData(ItemClassification.filler, 1, 89, "money"),
"Money x20": TunicItemData(ItemClassification.filler, 17, 90, "money"),
"Money x25": TunicItemData(ItemClassification.filler, 14, 91, "money"),
"Money x30": TunicItemData(ItemClassification.filler, 4, 92, "money"),
"Money x32": TunicItemData(ItemClassification.filler, 4, 93, "money"),
"Money x40": TunicItemData(ItemClassification.filler, 3, 94, "money"),
"Money x48": TunicItemData(ItemClassification.filler, 1, 95, "money"),
"Money x50": TunicItemData(ItemClassification.filler, 7, 96, "money"),
"Money x64": TunicItemData(ItemClassification.filler, 1, 97, "money"),
"Money x100": TunicItemData(ItemClassification.filler, 5, 98, "money"),
"Money x128": TunicItemData(ItemClassification.useful, 3, 99, "money"),
"Money x200": TunicItemData(ItemClassification.useful, 1, 100, "money"),
"Money x255": TunicItemData(ItemClassification.useful, 1, 101, "money"),
"Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "pages"),
"Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "pages"),
"Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "pages"),
"Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "pages"),
"Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "pages"),
"Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "pages"),
"Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "pages"),
"Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "pages"),
"Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "pages"),
"Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "pages"),
"Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "pages"),
"Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "pages"),
"Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "pages"),
"Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "pages"),
"Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "pages"),
"Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "pages"),
"Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "pages"),
"Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "pages"),
"Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "pages"),
"Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "pages"),
"Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "pages"),
"Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "pages"),
"Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "pages"),
"Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "pages"),
"Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "pages"),
"Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "pages"),
"Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "pages"),
"Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "pages"),
"Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "Hexagons"),
"Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "Hexagons"),
"Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "Hexagons"),
"Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "Hexagons"),
"ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "Offerings"),
"DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "Offerings"),
"Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "Offerings"),
"HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "Offerings"),
"MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "Offerings"),
"SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "Offerings"),
"Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "Hero Relics"),
"Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "Hero Relics"),
"Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "Hero Relics"),
"Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "Hero Relics"),
"Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "Hero Relics"),
"Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "Hero Relics"),
"Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "Cards"),
"Tincture": TunicItemData(ItemClassification.useful, 1, 58, "Cards"),
"Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "Cards"),
"Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "Cards"),
"Bracer": TunicItemData(ItemClassification.useful, 1, 61, "Cards"),
"Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "Cards"),
"Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "Cards"),
"Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "Cards"),
"Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "Cards"),
"Anklet": TunicItemData(ItemClassification.useful, 1, 66, "Cards"),
"Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "Cards"),
"Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "Cards"),
"Perfume": TunicItemData(ItemClassification.useful, 1, 69, "Cards"),
"Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "Cards"),
"Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "Cards"),
"Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "Cards"),
"Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "Golden Treasures"),
"Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "Golden Treasures"),
"Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "Golden Treasures"),
"Vintage": TunicItemData(ItemClassification.useful, 1, 76, "Golden Treasures"),
"Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "Golden Treasures"),
"Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "Golden Treasures"),
"Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "Golden Treasures"),
"Power Up": TunicItemData(ItemClassification.useful, 1, 80, "Golden Treasures"),
"Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "Golden Treasures"),
"Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "Golden Treasures"),
"Dusty": TunicItemData(ItemClassification.useful, 1, 83, "Golden Treasures"),
"Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "Golden Treasures"),
"Fool Trap": TunicItemData(ItemClassification.trap, 0, 85),
"Money x1": TunicItemData(ItemClassification.filler, 3, 86, "Money"),
"Money x10": TunicItemData(ItemClassification.filler, 1, 87, "Money"),
"Money x15": TunicItemData(ItemClassification.filler, 10, 88, "Money"),
"Money x16": TunicItemData(ItemClassification.filler, 1, 89, "Money"),
"Money x20": TunicItemData(ItemClassification.filler, 17, 90, "Money"),
"Money x25": TunicItemData(ItemClassification.filler, 14, 91, "Money"),
"Money x30": TunicItemData(ItemClassification.filler, 4, 92, "Money"),
"Money x32": TunicItemData(ItemClassification.filler, 4, 93, "Money"),
"Money x40": TunicItemData(ItemClassification.filler, 3, 94, "Money"),
"Money x48": TunicItemData(ItemClassification.filler, 1, 95, "Money"),
"Money x50": TunicItemData(ItemClassification.filler, 7, 96, "Money"),
"Money x64": TunicItemData(ItemClassification.filler, 1, 97, "Money"),
"Money x100": TunicItemData(ItemClassification.filler, 5, 98, "Money"),
"Money x128": TunicItemData(ItemClassification.useful, 3, 99, "Money"),
"Money x200": TunicItemData(ItemClassification.useful, 1, 100, "Money"),
"Money x255": TunicItemData(ItemClassification.useful, 1, 101, "Money"),
"Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "Pages"),
"Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "Pages"),
"Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "Pages"),
"Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "Pages"),
"Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "Pages"),
"Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "Pages"),
"Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "Pages"),
"Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "Pages"),
"Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "Pages"),
"Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "Pages"),
"Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "Pages"),
"Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "Pages"),
"Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "Pages"),
"Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "Pages"),
"Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "Pages"),
"Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "Pages"),
"Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "Pages"),
"Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "Pages"),
"Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "Pages"),
"Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "Pages"),
"Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "Pages"),
"Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "Pages"),
"Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "Pages"),
"Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "Pages"),
"Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "Pages"),
"Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "Pages"),
"Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "Pages"),
"Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "Pages"),
"Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "ladders"),
"Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "ladders"),
"Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "ladders"),
"Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "ladders"),
"Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "ladders"),
"Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "ladders"),
"Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "ladders"),
"Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "ladders"),
"Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "ladders"),
"Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "ladders"),
"Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "ladders"),
"Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "ladders"),
"Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "ladders"),
"Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "ladders"),
"Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "ladders"),
"Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "ladders"),
"Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "ladders"),
"Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "ladders"),
"Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "ladders"),
"Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "ladders"),
"Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "ladders"),
"Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "Ladders"),
"Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "Ladders"),
"Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "Ladders"),
"Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "Ladders"),
"Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "Ladders"),
"Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "Ladders"),
"Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "Ladders"),
"Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "Ladders"),
"Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "Ladders"),
"Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "Ladders"),
"Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "Ladders"),
"Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "Ladders"),
"Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "Ladders"),
"Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "Ladders"),
"Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "Ladders"),
"Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "Ladders"),
"Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "Ladders"),
"Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "Ladders"),
"Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "Ladders"),
"Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "Ladders"),
"Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"),
}
fool_tiers: List[List[str]] = [
@@ -220,20 +220,23 @@ item_name_groups: Dict[str, Set[str]] = {
# extra groups for the purpose of aliasing items
extra_groups: Dict[str, Set[str]] = {
"laurels": {"Hero's Laurels"},
"orb": {"Magic Orb"},
"dagger": {"Magic Dagger"},
"magic rod": {"Magic Wand"},
"holy cross": {"Pages 42-43 (Holy Cross)"},
"prayer": {"Pages 24-25 (Prayer)"},
"icebolt": {"Pages 52-53 (Icebolt)"},
"ice rod": {"Pages 52-53 (Icebolt)"},
"melee weapons": {"Stick", "Sword", "Sword Upgrade"},
"progressive sword": {"Sword Upgrade"},
"abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"},
"questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"},
"ladder to atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
"ladders to bell": {"Ladders to West Bell"},
"Laurels": {"Hero's Laurels"},
"Orb": {"Magic Orb"},
"Dagger": {"Magic Dagger"},
"Wand": {"Magic Wand"},
"Magic Rod": {"Magic Wand"},
"Fire Rod": {"Magic Wand"},
"Holy Cross": {"Pages 42-43 (Holy Cross)"},
"Prayer": {"Pages 24-25 (Prayer)"},
"Icebolt": {"Pages 52-53 (Icebolt)"},
"Ice Rod": {"Pages 52-53 (Icebolt)"},
"Melee Weapons": {"Stick", "Sword", "Sword Upgrade"},
"Progressive Sword": {"Sword Upgrade"},
"Abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"},
"Questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"},
"Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
"Ladders to Bell": {"Ladders to West Bell"},
"Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell
}
item_name_groups.update(extra_groups)

View File

@@ -1,11 +1,10 @@
from typing import Dict, NamedTuple, Set, Optional, List
from typing import Dict, NamedTuple, Set, Optional
class TunicLocationData(NamedTuple):
region: str
er_region: str # entrance rando region
location_group: Optional[str] = None
location_groups: Optional[List[str]] = None
location_base_id = 509342400
@@ -46,8 +45,8 @@ location_table: Dict[str, TunicLocationData] = {
"Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2 Lower"),
"Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"),
"Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"),
"East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="holy cross"),
"East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="holy cross"),
"East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"),
"East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"),
"East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"),
"East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"),
"East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"),
@@ -65,18 +64,18 @@ location_table: Dict[str, TunicLocationData] = {
"Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"),
"Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"),
"Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"),
"Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", location_group="holy cross"),
"Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", location_group="Holy Cross"),
"Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"),
"Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"),
"Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"),
"Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"),
"Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest", location_group="hero relic"),
"Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"),
"Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"),
"Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"),
"Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"),
"Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena", location_group="bosses"),
"Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena", location_group="Bosses"),
"Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"),
"Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", location_group="holy cross"),
"Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", location_group="Holy Cross"),
"Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
@@ -84,7 +83,7 @@ location_table: Dict[str, TunicLocationData] = {
"Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"),
"Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
"Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress", location_group="hero relic"),
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"),
@@ -101,8 +100,8 @@ location_table: Dict[str, TunicLocationData] = {
"Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="bosses"),
"Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="holy cross"),
"Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"),
"Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"),
"Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"),
"Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"),
"Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"),
@@ -110,7 +109,7 @@ location_table: Dict[str, TunicLocationData] = {
"Library Lab - Page 3": TunicLocationData("Library", "Library Lab"),
"Library Lab - Page 1": TunicLocationData("Library", "Library Lab"),
"Library Lab - Page 2": TunicLocationData("Library", "Library Lab"),
"Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library", location_group="hero relic"),
"Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"),
"Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"),
"Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"),
"Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"),
@@ -165,49 +164,49 @@ location_table: Dict[str, TunicLocationData] = {
"Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"),
"Shop - Potion 1": TunicLocationData("Overworld", "Shop", location_group="shop"),
"Shop - Potion 2": TunicLocationData("Overworld", "Shop", location_group="shop"),
"Shop - Coin 1": TunicLocationData("Overworld", "Shop", location_group="shop"),
"Shop - Coin 2": TunicLocationData("Overworld", "Shop", location_group="shop"),
"Shop - Potion 1": TunicLocationData("Overworld", "Shop"),
"Shop - Potion 2": TunicLocationData("Overworld", "Shop"),
"Shop - Coin 1": TunicLocationData("Overworld", "Shop"),
"Shop - Coin 2": TunicLocationData("Overworld", "Shop"),
"Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"),
"Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"),
"Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"),
"Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"),
"Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"),
"Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn Region"),
"Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"),
"Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"),
"Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"),
"Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"),
"Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="fairies"),
"Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="fairies"),
"Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"),
"Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="holy cross"),
"Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"),
"Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="holy cross"),
"Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="holy cross"),
"Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"),
"Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="holy cross"),
"Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"),
"Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"),
"Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Upper Overworld", location_group="holy cross"),
"Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", location_group="holy cross"),
"Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", location_group="holy cross"),
"Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"),
"Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"),
"Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"),
"Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", location_group="holy cross"),
"Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", location_group="holy cross"),
"Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", location_group="holy cross"),
"Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", location_group="holy cross"),
"Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", location_group="holy cross"),
"Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave Tower", location_group="holy cross"),
"Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", location_group="holy cross"),
"Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="holy cross"),
"Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="holy cross"),
"Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="holy cross"),
"Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
"Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
"Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
"Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
"Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="Fairies"),
"Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="Fairies"),
"Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
"Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="Holy Cross"),
"Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
"Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="Holy Cross"),
"Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="Holy Cross"),
"Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
"Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="Holy Cross"),
"Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
"Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
"Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Upper Overworld", location_group="Holy Cross"),
"Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", location_group="Holy Cross"),
"Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", location_group="Holy Cross"),
"Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"),
"Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"),
"Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"),
"Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", location_group="Holy Cross"),
"Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", location_group="Holy Cross"),
"Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", location_group="Holy Cross"),
"Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", location_group="Holy Cross"),
"Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", location_group="Holy Cross"),
"Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave Tower", location_group="Holy Cross"),
"Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", location_group="Holy Cross"),
"Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"),
"Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"),
"Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"),
"Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"),
"Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="holy cross"),
"Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"),
"Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"),
@@ -225,7 +224,7 @@ location_table: Dict[str, TunicLocationData] = {
"Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"),
"Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"),
"Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry", location_group="hero relics"),
"Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"),
"Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
@@ -246,7 +245,7 @@ location_table: Dict[str, TunicLocationData] = {
"Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="bosses"),
"Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="Bosses"),
"Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"),
"Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
@@ -288,14 +287,14 @@ location_table: Dict[str, TunicLocationData] = {
"Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp Mid"),
"Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp Front"),
"Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp Mid"),
"Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp", location_group="hero relic"),
"Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"),
"West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"),
"Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"),
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="holy cross"),
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="holy cross"),
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
"West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="holy cross"),
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
"West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"),
@@ -307,12 +306,12 @@ location_table: Dict[str, TunicLocationData] = {
"West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="bosses"),
"West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="Bosses"),
"West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"),
"West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"),
"West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"),
"West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"),
"Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden", location_group="hero relic"),
"Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
}
hexagon_locations: Dict[str, str] = {
@@ -325,7 +324,7 @@ location_name_to_id: Dict[str, int] = {name: location_base_id + index for index,
location_name_groups: Dict[str, Set[str]] = {}
for loc_name, loc_data in location_table.items():
loc_group_name = loc_name.split(" - ", 1)[0]
location_name_groups.setdefault(loc_group_name, set()).add(loc_name)
if loc_data.location_group:
if loc_data.location_group not in location_name_groups.keys():
location_name_groups[loc_data.location_group] = set()
location_name_groups[loc_data.location_group].add(loc_name)
location_name_groups.setdefault(loc_data.location_group, set()).add(loc_name)

View File

@@ -103,8 +103,10 @@ class ExtraHexagonPercentage(Range):
class EntranceRando(TextChoice):
"""
Randomize the connections between scenes.
If you set this to a value besides true or false, that value will be used as a custom seed.
A small, very lost fox on a big adventure.
If you set this option's value to a string, it will be used as a custom seed.
Every player who uses the same custom seed will have the same entrances, choosing the most restrictive settings among these players for the purpose of pairing entrances.
"""
internal_name = "entrance_rando"
display_name = "Entrance Rando"

View File

@@ -329,7 +329,7 @@ def set_normal_rules(world: "YoshisIslandWorld") -> None:
set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player))
set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player))
set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: state.has("Super Star", player))
set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player))
set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player))
set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch", "Egg Launcher"}, player))