Compare commits

..

72 Commits

Author SHA1 Message Date
NewSoupVi 80e89db812 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:34:23 +01:00
NewSoupVi 8d0f1be791 Update world api.md 2024-10-28 22:25:07 +01:00
NewSoupVi febcd84995 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:24:00 +01:00
NewSoupVi d0a555a185 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:23:36 +01:00
NewSoupVi da72d40f14 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:23:04 +01:00
NewSoupVi f78f906ced line break issues 2024-10-28 21:20:39 +01:00
NewSoupVi 245a66828d Update world api.md 2024-10-28 21:18:17 +01:00
NewSoupVi 798556fd3f Update world api.md 2024-10-28 21:16:47 +01:00
NewSoupVi 14609100cf Write a big motivation paragraph 2024-10-28 21:14:33 +01:00
NewSoupVi fe366e2985 Update world api.md 2024-10-28 21:03:40 +01:00
NewSoupVi 819710ff61 Reorganize / Rewrite the parts about optimisations a bit 2024-10-28 21:00:27 +01:00
NewSoupVi c0cd03f436 Update world api.md 2024-09-27 01:21:11 +02:00
NewSoupVi e694804509 Update world api.md 2024-09-27 01:20:47 +02:00
NewSoupVi c21c10a602 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-09-25 13:13:06 +02:00
NewSoupVi 54ab181514 Code corrections / actually follow own spec 2024-09-22 15:27:49 +02:00
NewSoupVi 88de34dfd7 Update world api.md 2024-09-22 15:26:00 +02:00
NewSoupVi 555b7211ef Update docs/world api.md 2024-09-21 19:13:41 +02:00
NewSoupVi 7b097c918e Update docs/world api.md
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-21 19:13:10 +02:00
NewSoupVi 4eeac945fb Update world api.md 2024-09-21 19:03:24 +02:00
NewSoupVi db577ad899 Update world api.md 2024-09-21 19:03:01 +02:00
NewSoupVi 8aed3efe97 Update world api.md 2024-09-21 19:02:30 +02:00
NewSoupVi f7d6f689fb Update world api.md 2024-09-21 19:00:57 +02:00
NewSoupVi 910d4b93c9 Update world api.md 2024-09-21 19:00:03 +02:00
NewSoupVi 584dd8fe75 Update world api.md 2024-09-21 18:59:36 +02:00
NewSoupVi 259d010a86 Update world api.md 2024-09-21 18:56:46 +02:00
NewSoupVi 98d808bb87 Update world api.md 2024-09-21 18:55:57 +02:00
NewSoupVi f9108f4331 Update world api.md 2024-09-21 18:52:02 +02:00
NewSoupVi 4b6ad12192 Update world api.md 2024-09-21 18:51:30 +02:00
NewSoupVi a08fbc66a8 Docs: Make an actual LogicMixin spec & explanation 2024-09-21 18:46:56 +02:00
qwint 41ddb96b24 HK: add race bool to slot data (#3971) 2024-09-21 16:45:22 +02:00
João Victor ba8f03516e Docs: added Brazilian Portuguese Translation for Hollow Knight setup guide (#3909)
* add neww pt-br translation

* setup file

* Update setup_pt_br.md

* add ` to paths

* correct grammar

* add space .-.

* add more spaces .-. .-. .-.

* capitalize HK

* Update setup_pt_br.md

* accent not the same as punctuation

* small changes

* Update setup_pt_br.md
2024-09-20 19:19:48 +02:00
Spineraks 0095eecf2b Yacht Dice: Remove Victory item and make it an event instead (#3968)
* Add the yacht dice (from other git) world to the yacht dice fork

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df617.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

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

* Apply suggestions from code review

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

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb3.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

* Yacht Dice: setup: change release-link to latest

On the installation page, link to the latest release, instead of the page with all releases

* Several fixes and changes

-change apworld version
-Removed the extra roll (this was not intended)
-change extra_points_added to a mutable list to that it actually does something
-removed variables multipliers_added and items_added
-Rules, don't order by quantity, just by mean_score
-Changed the weights in general to make it faster

* 🐶

* Revert setup to what it was (latest, without S)

* remove temp weights file, shouldn't be here

* Made sure that there is not too many step score multipliers.

Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game.

* add filler item name

* Textual fixes and changes

* Remove Victory item and use event instead.

* Revert "Remove Victory item and use event instead."

This reverts commit c2f7d674d3.

* Revert "Textual fixes and changes"

This reverts commit e9432f9245.

* Remove Victory item and make it an event instead

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-20 19:07:45 +02:00
Alex Nordstrom 79942c09c2 LADX: define filler item, fix for extra golden leaves (#3918)
* set filler item
also rename "Master Stalfos' Message" to "Nothing" as it shows up in game, and "Gel" to "Zol Attack"

* fix for extra gold leaves

* fix for start_inventory
2024-09-20 16:18:09 +02:00
digiholic 1b15c6920d [OSRS] Adds display names to Options #3954 2024-09-20 16:15:30 +02:00
gaithern 499d79f089 Kingdom Hearts: Fix Hint Spam and Add Setting Queries #3899 2024-09-19 22:32:47 +02:00
NewSoupVi 926e08513c The Witness: Remove some unused code #3852 2024-09-19 01:57:59 +02:00
Silvris 025c550991 Ocarina of Time: options and general cleanup (#3767)
* working?

* missed one

* fix old start inventory usage

* missed global random usage

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 21:26:59 +02:00
Doug Hoskisson fced9050a4 Zillion: fix logic cache (#3719) 2024-09-18 21:09:47 +02:00
Faris 2ee8b7535d OSRS: UT integration for OSRS to support chunksanity (#3776) 2024-09-18 20:53:17 +02:00
Remy Jette 0d35cd4679 BizHawkClient: Avoid error launching BizHawkClient via Launcher CLI (#3554)
* Core, BizHawkClient: Support launching BizHawkClient via Launcher command line

* Revert changes to LauncherComponents.py
2024-09-18 20:42:22 +02:00
Alchav db5d9fbf70 Pokemon R/B: Version 5 Update (#3566)
* Quiz updates

* Enable Partial Trainersanity

* Losable Key Items Still Count

* New options api

* Type Chart Seed

* Continue switching to new options API

* Level Scaling and Quiz fixes

* Level Scaling and Quiz fixes

* Clarify that palettes are only for Super Gameboy

* Type chart seed groups use one random players' options

* remove goal option again

* Text updates

* Trainersanity Trainers ignore Blind Trainers setting

* Re-order simple connecting interiors so that directions are preserved when possible

* Dexsanity exact number

* Year update

* Dexsanity Doc update

* revert accidental file deletion

* Fixes

* Add world parameter to logic calls

* restore correct seeded random object

* missing world.options changes

* Trainersanity table bug fix

* delete entrances as well as exits when restarting door shuffle

* Do not collect route 25 item for level scaling if trainer is trainersanity

* world.options in level_scaling.py

* Update worlds/pokemon_rb/level_scaling.py

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

* Update worlds/pokemon_rb/encounters.py

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

* Update worlds/pokemon_rb/encounters.py

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

* world -> multiworld

* Fix Cerulean Cave Hidden Item Center Rocks region

* Fix Cerulean Cave Hidden Item Center Rocks region for real

* Remove "self-locking" rules

* Update worlds/pokemon_rb/regions.py

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

* Fossil events

* Update worlds/pokemon_rb/level_scaling.py

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

---------

Co-authored-by: alchav <alchav@jalchavware.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 20:37:17 +02:00
jamesbrq 51a6dc150c MLSS: Various bugfixes and QoL updates (#3744)
* Small fixes

* Update Location names + Remove redundant rule

* Fix for str not being returned in get_filler_item_name()

* ASM changes + various name/logic updates

* Remove extra unintended change + Make beanstone/beanlets useful

* Add missing timer logic to client

* Update Rules.py

* Fix bad capitalization

* Small formatting and ASM changes

* Update basepatch.bsdiff

* Update seed verification to be more likely to make a correct comparison

* Add Pipe 10

* Final batch of small fixes

* FINAL CHANGE I SWEAR

* Added victory Item for spoilers

* Update worlds/mlss/Regions.py

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

* Update worlds/mlss/Items.py

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

* Fix jokes end logic

* Update worlds/mlss/Regions.py

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>

* Update worlds/mlss/Rules.py

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>

* Fix jokes end logic

* Item Location mismatch + Check options against rules

* Change List to Set + Check options against rules

* Moved Victory item to event

* Update worlds/mlss/__init__.py

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Update worlds/mlss/__init__.py

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 19:33:02 +02:00
black-sliver 710609fa60 WebHost: move api/room_status out of __init__.py (#3958)
* WebHost: move room_status out of __init__.py

The old location is unexpected and easy to miss.

* WebHost: fix typing in api/room_status
2024-09-18 10:27:53 +02:00
Fabian Dill da781bb4ac Core: rename yaml_output to csv_output (#3955) 2024-09-18 04:37:10 +02:00
Aaron Wagener 69487661dd Core: change yaml_output to output a full csv (#3653)
* make yaml_output arg a bool instead of number

* make yaml_output dump all player options as csv

* it sorts by game so swap those columns

* capitalize game and name headers

* use a list and just add an if before adding instead of sorting

* skip options that the world doesn't want displayed

* check if the class is a subclass of Removed specifically instead of the none flag

* don't create empty rows

* add a header for every game option that isn't from the common ones even if they have the same name

* add to webhost gen args so it can still gen
2024-09-18 01:33:03 +02:00
black-sliver f73c0d9894 WebHost: Better host room v2 (#3948)
* WebHost: add spinner to room command

and show error message if fetch fails due to NetworkError

* WebHost: don't update room log while tab is inactive

* WebHost: don't include log for automated requests

* WebHost: refresh room also for re-spinups

and do that from javascript

* Test, WebHost: send fake user-agent where required

* WebHost: remove wrong comment in host room
2024-09-18 00:47:26 +02:00
Fabian Dill 6fac83b84c Factorio: update API use (#3760)
---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-18 00:18:17 +02:00
sgrunt debb936618 DOOM II: Fix sector 95 assignment in DOOM II MAP17 to correctly flag the BFG9000 location as in the Yellow Key area (#3705)
Co-authored-by: sgrunt <sgrunt1987@gmail.com>
2024-09-18 00:08:18 +02:00
agilbert1412 8c5b65ff26 Stardew Valley: Remove Accessibility and progression balancing from presets #3833 2024-09-18 00:07:40 +02:00
agilbert1412 a7c96436d9 Stardew valley: Add Marlon bedroom entrance rule (#3735)
* - Created a test for the "Mapping Cave Systems" book

* - Added missing rule to marlon's bedroom

* - Can kill any monster, not just green slime

* - Added a compound source structure, but I ended up deciding to not use it here. Still keeping it as it will probably be useful eventually

* - Use the compound source of the monster compoundium (ironic, I know)

* - Add required elevators

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 00:03:33 +02:00
Aaron Wagener 4e60f3cc54 The Messenger: Fix Portal Plando Issues (#3838)
* add a more clear error message for a missing exit

* remove portal region from the available pool

* ensure plando portals are in the correct spot in the list and it gets cleared correctly
2024-09-18 00:00:26 +02:00
Exempt-Medic 30a0b337a2 DS3: Make Red Eye Orb always require Lift Chamber Key #3857 2024-09-17 23:58:45 +02:00
Scipio Wright 4ea1dddd2f TUNIC: Better logic for Library Lab glass and Fortress leaf piles #3880 2024-09-17 23:57:55 +02:00
Mrks dc218b7997 LADX: Adding Slot Data For Magpie Tracker (#3582)
* wip: LADX slot_data

* LADX: slot_data

* Sending slot_data to magpie.

* Moved sending slot_data from pushing to pull by Magpie request.

* Adding EoF newline to tracker.py.

* Update Tracker.py

* Update __init__.py

* Update LinksAwakeningClient.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-17 23:56:40 +02:00
Natalie Weizenbaum 78c5489189 DS3: Mark the Archdeacon Set as downstream of Deacons of the Deep (#3883)
This ensures that if Deacons is replaced with Yhorm, the Storm Ruler
won't show up in these locations.
2024-09-17 23:50:02 +02:00
Bryce Wilson d1a7bc66e6 Pokemon Emerald: Prevent client from spamming goal status update (#3900) 2024-09-17 23:49:36 +02:00
Ziktofel b982e9ebb4 SC2: Fix /received display bugs (#3949)
* SC2: Fix location display in /received command

* SC2: Backport broken markup fix in /received output from the dev branch

* Cleanup
2024-09-17 23:18:43 +02:00
Bryce Wilson 8f7e0dc441 Core: Improve death link option description (#3951) 2024-09-17 23:17:41 +02:00
Bryce Wilson 5aea8d4ab5 Pokemon Emerald: Update changelog (#3952) 2024-09-17 15:14:05 +02:00
Rensen3 97be5f1dde YGO06: slotdata fix (#3953)
* YGO06: fix slot data for universal tracker

* YGO06: put Extremely Low Deck Bonus after Low Deck Bonus
2024-09-17 15:13:19 +02:00
Mysteryem dae3fe188d OOT: Fix incorrect region accessibility after update_reachable_regions() (#3712)
`CollectionState.update_reachable_regions()` un-stales the state for all
players, but when checking `OOTRegion.can_reach()`, it would only update
OOT's age region accessibility when the state was stale, so if the state
was always un-staled by `update_reachable_regions()` immediately before
`OOTRegion.can_reach()`, OOT's age region accessibility would never
update.

This patch fixes the issue by replacing use of CollectionState.stale
with a separate stale state dictionary specific to OOT that is only
un-staled by `_oot_update_age_reachable_regions()`.

OOT's collect() and remove() implementations have been updated to stale
the new OOT-specific state.
2024-09-17 15:11:35 +02:00
Exempt-Medic 96542fb2d8 Blasphemous: Move pre_fill to create_items #3901 2024-09-17 15:08:15 +02:00
qwint ec50b0716a Core: Add color conversions for colorama/terminal output #3940 2024-09-17 14:44:32 +02:00
Bryce Wilson f8d3c26e3c Pokemon Emerald: Fix unguarded wonder trade write (#3939) 2024-09-17 14:43:22 +02:00
digiholic 1c0cec0de2 [OSRS] Adds Description to OSRS World #3921 2024-09-17 14:42:48 +02:00
Silvris 4692e6f08a MM2: fix Air Shooter minimum damage #3922 2024-09-17 14:42:19 +02:00
Mysteryem b8d23ec595 MMBN3: Add missing indirect conditions (#3931)
Entrances to SciLab_Cyberworld and Yoka_Cyberworld had logic for being
able to reach SciLab_Overworld, but did not register this indirect
condition.

Entrances to Beach_Cyberworld had logic for being able to reach
Yoka_Overworld, but did not register this indirect condition.

Entrances to Undernet and Secret_Area had logic for having a high enough
explore score, but explore score is calculated based on the
accessibility of a number of regions and no indirect conditions were
being registered for these regions.
2024-09-17 14:41:56 +02:00
Silvris ce42e42af7 Core: fix single player item links (#3721)
* fix single player item links

* Make a variable and fix weird spacing

* use advancement instead of classification

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-17 14:36:05 +02:00
Star Rauchenberger ee12dda361 Lingo: Added missing connection from The Tenacious -> Hub Room (#3947) 2024-09-16 18:06:20 +02:00
Exempt-Medic 84805a4e54 HK: XBox doesn't exist #3932 2024-09-16 14:30:47 +02:00
Fabian Dill 5530d181da Core: update version number (#3944) 2024-09-16 06:48:13 +02:00
Mysteryem ed948e3e5b sm64ex: Add missing indirect condition for BitFS randomized entrance (#3926)
The Bowser in the Fire Sea randomized entrance has an access rule that
requires being able to reach "DDD: Board Bowser's Sub", but being able
to reach a location also requires being able to reach the region that
location is in, so an indirect condition is required.
2024-09-13 16:02:13 +02:00
109 changed files with 3228 additions and 2298 deletions
+9 -20
View File
@@ -342,6 +342,8 @@ class MultiWorld():
region = Region("Menu", group_id, self, "ItemLink")
self.regions.append(region)
locations = region.locations
# ensure that progression items are linked first, then non-progression
self.itempool.sort(key=lambda item: item.advancement)
for item in self.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
@@ -1204,26 +1206,13 @@ class Location:
class ItemClassification(IntFlag):
filler = 0b0000
""" aka trash, as in filler items like ammo, currency etc """
progression = 0b0001
""" Item that is logically relevant.
Protects this item from being placed on excluded or unreachable locations. """
useful = 0b0010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """
trap = 0b0100
""" Item that is detrimental in some way. """
skip_balancing = 0b1000
""" should technically never occur on its own
Item that is logically relevant, but progression balancing should not touch.
Typically currency or other counted items. """
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
trap = 0b0100 # detrimental item
skip_balancing = 0b1000 # should technically never occur on its own
# Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
progression_skip_balancing = 0b1001 # only progression gets balanced
def as_flag(self) -> int:
+2 -1
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
import sys
import ModuleUpdate
ModuleUpdate.update()
from worlds._bizhawk.context import launch
if __name__ == "__main__":
launch()
launch(*sys.argv[1:])
+10 -12
View File
@@ -475,28 +475,26 @@ def distribute_items_restrictive(multiworld: MultiWorld,
nonlocal lock_later
lock_later.append(location)
single_player = multiworld.players == 1 and not multiworld.groups
if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
if progitempool:
# "advancement/progression fill"
if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
name="Progression", single_player_placement=single_player)
elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
name="Progression", single_player_placement=single_player)
elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
allow_partial=True, name="Progression", single_player_placement=single_player)
if progitempool:
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
@@ -529,7 +527,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than excludable items.",
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
multiworld=multiworld,
)
+5 -26
View File
@@ -43,10 +43,10 @@ def mystery_argparse():
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults.plando_options,
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument("--csv_output", action="store_true",
help="Output rolled player options to csv (made for async multiworld).")
parser.add_argument("--plando", default=defaults.plando_options,
help="List of options that can be set manually. Can be combined, for example \"bosses, items\"")
parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.")
parser.add_argument("--skip_output", action="store_true",
@@ -156,6 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
erargs.name = {}
erargs.csv_output = args.csv_output
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
@@ -216,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
else:
logging.debug(f"No player settings defined for option '{option}'")
else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)
return erargs, seed
+5
View File
@@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext):
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient()
self.slot_data = {}
if magpie:
self.magpie_enabled = True
self.magpie = MagpieBridge()
@@ -564,6 +566,8 @@ class LinksAwakeningContext(CommonContext):
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {})
# TODO - use watcher_event
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
@@ -628,6 +632,7 @@ class LinksAwakeningContext(CommonContext):
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
self.magpie.slot_data = self.slot_data
except Exception:
# Don't let magpie errors take out the client
pass
+3
View File
@@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multiworld.sprite_pool = args.sprite_pool.copy()
multiworld.set_options(args)
if args.csv_output:
from Options import dump_player_options
dump_player_options(multiworld)
multiworld.set_item_links()
multiworld.state = CollectionState(multiworld)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
+2 -1
View File
@@ -273,7 +273,8 @@ class RawJSONtoTextParser(JSONtoTextParser):
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47,
'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors
def color_code(*args):
+43 -3
View File
@@ -8,16 +8,17 @@ import numbers
import random
import typing
import enum
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass
from schema import And, Optional, Or, Schema
from typing_extensions import Self
from Utils import get_fuzzy_results, is_iterable_except_str
from Utils import get_fuzzy_results, is_iterable_except_str, output_path
if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
from BaseClasses import MultiWorld, PlandoOptions
from worlds.AutoWorld import World
import pathlib
@@ -1335,7 +1336,7 @@ class PriorityLocations(LocationSet):
class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too."""
"""When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
display_name = "Death Link"
rich_text_doc = True
@@ -1532,3 +1533,42 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
f.write(res)
def dump_player_options(multiworld: MultiWorld) -> None:
from csv import DictWriter
game_players = defaultdict(list)
for player, game in multiworld.game.items():
game_players[game].append(player)
game_players = dict(sorted(game_players.items()))
output = []
per_game_option_names = [
getattr(option, "display_name", option_key)
for option_key, option in PerGameCommonOptions.type_hints.items()
]
all_option_names = per_game_option_names.copy()
for game, players in game_players.items():
game_option_names = per_game_option_names.copy()
for player in players:
world = multiworld.worlds[player]
player_output = {
"Game": multiworld.game[player],
"Name": multiworld.get_player_name(player),
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
if issubclass(Removed, option):
continue
display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name
if display_name not in game_option_names:
all_option_names.append(display_name)
game_option_names.append(display_name)
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
fields = ["Game", "Name", *all_option_names]
writer = DictWriter(file, fields)
writer.writeheader()
writer.writerows(output)
+1 -1
View File
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.5.0"
__version__ = "0.5.1"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
+3 -39
View File
@@ -1,51 +1,15 @@
"""API endpoints package."""
from typing import List, Tuple
from uuid import UUID
from flask import Blueprint, abort, url_for
from flask import Blueprint
import worlds.Files
from ..models import Room, Seed
from ..models import Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
# unsorted/misc endpoints
def get_players(seed: Seed) -> List[Tuple[str, str]]:
return [(slot.player_name, slot.game) for slot in seed.slots]
@api_endpoints.route('/room_status/<suuid:room>')
def room_info(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
def supports_apdeltapatch(game: str):
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}
from . import generate, user, datapackage # trigger registration
from . import datapackage, generate, room, user # trigger registration
+42
View File
@@ -0,0 +1,42 @@
from typing import Any, Dict
from uuid import UUID
from flask import abort, url_for
import worlds.Files
from . import api_endpoints, get_players
from ..models import Room
@api_endpoints.route('/room_status/<suuid:room_id>')
def room_info(room_id: UUID) -> Dict[str, Any]:
room = Room.get(id=room_id)
if room is None:
return abort(404)
def supports_apdeltapatch(game: str) -> bool:
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}
+1
View File
@@ -134,6 +134,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
{"bosses", "items", "connections", "texts"}))
erargs.skip_prog_balancing = False
erargs.skip_output = False
erargs.csv_output = False
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
+25 -10
View File
@@ -132,26 +132,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
@app.post("/room/<suuid:room>")
def host_room_command(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))
@app.get("/room/<suuid:room>")
def host_room(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
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
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
with db_session:
room.last_activity = now # will trigger a spinup, if it's not already running
def get_log(max_size: int = 1024000) -> str:
browser_tokens = "Mozilla", "Chrome", "Safari"
automated = ("update" in request.args
or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0:
return ""
try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0
+25
View File
@@ -58,3 +58,28 @@
overflow-y: auto;
max-height: 400px;
}
.loader{
display: inline-block;
visibility: hidden;
margin-left: 5px;
width: 40px;
aspect-ratio: 4;
--_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0);
background:
var(--_g) 0 50%,
var(--_g) 50% 50%,
var(--_g) 100% 50%;
background-size: calc(100%/3) 100%;
animation: l7 1s infinite linear;
}
.loader.loading{
visibility: visible;
}
@keyframes l7{
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%}
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 }
}
+104 -51
View File
@@ -19,28 +19,30 @@
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="host-room">
{% if room.owner == session["_id"] %}
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
<br />
{% endif %}
{% if room.tracker %}
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
<br />
{% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
{% if room.last_port == -1 %}
There was an error hosting this Room. Another attempt will be made on refreshing this page.
The most likely failure reason is that the multiworld is too old to be loaded now.
{% elif room.last_port %}
You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
</span>
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
{% endif %}
<span id="host-room-info">
{% if room.owner == session["_id"] %}
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
<br />
{% endif %}
{% if room.tracker %}
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
<br />
{% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
{% if room.last_port == -1 %}
There was an error hosting this Room. Another attempt will be made on refreshing this page.
The most likely failure reason is that the multiworld is too old to be loaded now.
{% elif room.last_port %}
You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
</span>
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
{% endif %}
</span>
{{ macros.list_patches_room(room) }}
{% if room.owner == session["_id"] %}
<div style="display: flex; align-items: center;">
@@ -49,6 +51,7 @@
<label for="cmd"></label>
<input class="form-control" type="text" id="cmd" name="cmd"
placeholder="Server Command. /help to list them, list gets appended to log.">
<span class="loader"></span>
</div>
</form>
<a href="{{ url_for("display_log", room=room.id) }}">
@@ -62,6 +65,7 @@
let url = '{{ url_for('display_log', room = room.id) }}';
let bytesReceived = {{ log_len }};
let updateLogTimeout;
let updateLogImmediately = false;
let awaitingCommandResponse = false;
let logger = document.getElementById("logger");
@@ -78,29 +82,36 @@
async function updateLog() {
try {
let res = await fetch(url, {
headers: {
'Range': `bytes=${bytesReceived}-`,
}
});
if (res.ok) {
let text = await res.text();
if (text.length > 0) {
awaitingCommandResponse = false;
if (bytesReceived === 0 || res.status !== 206) {
logger.innerHTML = '';
}
if (res.status !== 206) {
bytesReceived = 0;
} else {
bytesReceived += new Blob([text]).size;
}
if (logger.innerHTML.endsWith('…')) {
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
}
logger.appendChild(document.createTextNode(text));
scrollToBottom(logger);
if (!document.hidden) {
updateLogImmediately = false;
let res = await fetch(url, {
headers: {
'Range': `bytes=${bytesReceived}-`,
}
});
if (res.ok) {
let text = await res.text();
if (text.length > 0) {
awaitingCommandResponse = false;
if (bytesReceived === 0 || res.status !== 206) {
logger.innerHTML = '';
}
if (res.status !== 206) {
bytesReceived = 0;
} else {
bytesReceived += new Blob([text]).size;
}
if (logger.innerHTML.endsWith('…')) {
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
}
logger.appendChild(document.createTextNode(text));
scrollToBottom(logger);
let loader = document.getElementById("command-form").getElementsByClassName("loader")[0];
loader.classList.remove("loading");
}
}
} else {
updateLogImmediately = true;
}
}
finally {
@@ -125,20 +136,62 @@
});
ev.preventDefault(); // has to happen before first await
form.reset();
let res = await req;
if (res.ok || res.type === 'opaqueredirect') {
awaitingCommandResponse = true;
window.clearTimeout(updateLogTimeout);
updateLogTimeout = window.setTimeout(updateLog, 100);
} else {
window.alert(res.statusText);
let loader = form.getElementsByClassName("loader")[0];
loader.classList.add("loading");
try {
let res = await req;
if (res.ok || res.type === 'opaqueredirect') {
awaitingCommandResponse = true;
window.clearTimeout(updateLogTimeout);
updateLogTimeout = window.setTimeout(updateLog, 100);
} else {
loader.classList.remove("loading");
window.alert(res.statusText);
}
} catch (e) {
console.error(e);
loader.classList.remove("loading");
window.alert(e.message);
}
}
document.getElementById("command-form").addEventListener("submit", postForm);
updateLogTimeout = window.setTimeout(updateLog, 1000);
logger.scrollTop = logger.scrollHeight;
document.addEventListener("visibilitychange", () => {
if (!document.hidden && updateLogImmediately) {
updateLog();
}
})
</script>
{% endif %}
<script>
function updateInfo() {
let url = new URL(window.location.href);
url.search = "?update";
fetch(url)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
return res.text()
})
.then(text => new DOMParser().parseFromString(text, 'text/html'))
.then(newDocument => {
let el = newDocument.getElementById("host-room-info");
document.getElementById("host-room-info").innerHTML = el.innerHTML;
});
}
if (document.querySelector("meta[http-equiv='refresh']")) {
console.log("Refresh!");
window.addEventListener('load', function () {
for (let i=0; i<3; i++) {
window.setTimeout(updateInfo, Math.pow(2, i) * 2000); // 2, 4, 8s
}
window.stop(); // cancel meta refresh
})
}
</script>
</div>
{% endblock %}
+1 -1
View File
@@ -510,7 +510,7 @@ In JSON this may look like:
| ----- | ----- |
| 0 | Nothing special about this item |
| 0b001 | If set, indicates the item can unlock logical advancement |
| 0b010 | If set, indicates the item is especially useful |
| 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
| 0b100 | If set, indicates the item is a trap |
### JSONMessagePart
+87 -5
View File
@@ -248,8 +248,7 @@ will all have the same ID. Name must not be numeric (must contain at least 1 let
Other classifications include:
* `filler`: a regular item or trash item
* `useful`: item that is especially useful. Cannot be placed on excluded or unreachable locations. When combined with
another flag like "progression", it means "an especially useful progression item".
* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
* `trap`: negative impact on the player
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
combined with `progression`; see below)
@@ -697,9 +696,92 @@ When importing a file that defines a class that inherits from `worlds.AutoWorld.
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
world since the namespace is shared with all other logic mixins.
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
with the state.
Please do this with caution and only when necessary.
LogicMixin is handy when your logic is more complex than one-to-one location-item relationships.
A game in which "The red key opens the red door" can just express this relationship through a one-line access rule.
But now, consider a game with a heavy focus on combat, where the main logical consideration is which enemies you can
defeat with your current items.
There could be dozens of weapons, armor pieces, or consumables that each improve your ability to defeat
specific enemies to varying degrees. It would be useful to be able to keep track of "defeatable enemies" as a state variable,
and have this variable be recalculated as necessary based on newly collected/removed items.
This is the capability of LogicMixin: Adding custom variables to state that get recalculated as necessary.
In general, a LogicMixin class should have at least one mutable variable that is tracking some custom state per player,
as well as `init_mixin` and `copy_mixin` functions so that this variable gets initialized and copied correctly when
`CollectionState()` and `CollectionState.copy()` are called respectively.
```python
from BaseClasses import CollectionState, MultiWorld
from worlds.AutoWorld import LogicMixin
class MyGameState(LogicMixin):
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
def init_mixin(self, multiworld: MultiWorld) -> None:
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
# You can also use something like Collections.defaultdict
self.mygame_defeatable_enemies = {
player: set() for player in multiworld.get_game_players("My Game")
}
def copy_mixin(self, new_state: CollectionState) -> CollectionState:
# Be careful to make a "deep enough" copy here!
new_state.mygame_defeatable_enemies = {
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
}
```
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
Usually, doing this coincides with an override of `World.collect` and `World.remove`, where the custom state variable
gets recalculated when a relevant item is collected or removed.
```python
# __init__.py
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change and item in COMBAT_ITEMS:
state.mygame_defeatable_enemies[self.player] |= get_newly_unlocked_enemies(state)
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change and item in COMBAT_ITEMS:
state.mygame_defeatable_enemies[self.player] -= get_newly_locked_enemies(state)
return change
```
Using LogicMixin can greatly slow down your code if you don't use it intelligently. This is because `collect`
and `remove` are called very frequently during fill. If your `collect` & `remove` cause a heavy calculation
every time, your code might end up being *slower* than just doing calculations in your access rules.
One way to optimise recalculations is to make use of the fact that `collect` should only unlock things,
and `remove` should only lock things.
In our example, we have two different functions: `get_newly_unlocked_enemies` and `get_newly_locked_enemies`.
`get_newly_unlocked_enemies` should only consider enemies that are *not already in the set*
and check whether they were **unlocked**.
`get_newly_locked_enemies` should only consider enemies that are *already in the set*
and check whether they **became locked**.
Another impactful way to optimise LogicMixin is to use caching.
Your custom state variables don't actually need to be recalculated on every `collect` / `remove`, because there are
often multiple calls to `collect` / `remove` between access rule calls. Thus, it would be much more efficient to hold
off on recaculating until the an actual access rule call happens.
A common way to realize this is to define a `mygame_state_is_stale` variable that is set to True in `collect`, `remove`,
and `init_mixin`. The calls to the actual recalculating functions are then moved to the start of the relevant
access rules like this:
```python
def can_defeat_enemy(state: CollectionState, player: int, enemy: str) -> bool:
if state.mygame_state_is_stale[player]:
state.mygame_defeatable_enemies[player] = recalculate_defeatable_enemies(state)
state.mygame_state_is_stale[player] = False
return enemy in state.mygame_defeatable_enemies[player]
```
Only use LogicMixin if necessary. There are often other ways to achieve what it does, like making clever use of
`state.prog_items`, using event items, pseudo-regions, etc.
#### pre_fill
+2 -1
View File
@@ -131,7 +131,8 @@ class TestHostFakeRoom(TestBase):
f.write(text)
with self.app.app_context(), self.app.test_request_context():
response = self.client.get(url_for("host_room", room=self.room_id))
response = self.client.get(url_for("host_room", room=self.room_id),
headers={"User-Agent": "Mozilla/5.0"})
response_text = response.get_data(True)
self.assertEqual(response.status_code, 200)
self.assertIn("href=\"/seed/", response_text)
+1 -1
View File
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def launch_client(*args) -> None:
from .context import launch
launch_subprocess(launch, name="BizHawkClient")
launch_subprocess(launch, name="BizHawkClient", args=args)
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
+2 -2
View File
@@ -239,11 +239,11 @@ async def _patch_and_run_game(patch_file: str):
logger.exception(exc)
def launch() -> None:
def launch(*launch_args) -> None:
async def main():
parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
args = parser.parse_args()
args = parser.parse_args(launch_args)
ctx = BizHawkClientContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
+1 -3
View File
@@ -199,8 +199,6 @@ class BlasphemousWorld(World):
self.multiworld.itempool += pool
def pre_fill(self):
self.place_items_from_dict(unrandomized_dict)
if self.options.thorn_shuffle == "vanilla":
@@ -335,4 +333,4 @@ class BlasphemousItem(Item):
class BlasphemousLocation(Location):
game: str = "Blasphemous"
game: str = "Blasphemous"
+3
View File
@@ -63,6 +63,9 @@ all_bosses = [
DS3BossInfo("Deacons of the Deep", 3500800, locations = {
"CD: Soul of the Deacons of the Deep",
"CD: Small Doll - boss drop",
"CD: Archdeacon White Crown - boss room after killing boss",
"CD: Archdeacon Holy Garb - boss room after killing boss",
"CD: Archdeacon Skirt - boss room after killing boss",
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
}),
DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = {
+1 -3
View File
@@ -612,9 +612,7 @@ class DarkSouls3World(World):
self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows")
# Define the access rules to some specific locations
if self._is_location_available("FS: Lift Chamber Key - Leonhard"):
self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss",
"Lift Chamber Key")
self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", "Lift Chamber Key")
self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit",
"Jailbreaker's Key")
self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key")
+1 -1
View File
@@ -1470,7 +1470,7 @@ location_table: Dict[int, LocationDict] = {
'map': 6,
'index': 102,
'doom_type': 2006,
'region': "Tenements (MAP17) Main"},
'region': "Tenements (MAP17) Yellow"},
361243: {'name': 'Tenements (MAP17) - Plasma gun',
'episode': 2,
'map': 6,
+20 -19
View File
@@ -1,5 +1,6 @@
"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
import dataclasses
import json
import os
import shutil
@@ -88,6 +89,8 @@ class FactorioModFile(worlds.Files.APContainer):
def generate_mod(world: "Factorio", output_directory: str):
player = world.player
multiworld = world.multiworld
random = world.random
global data_final_template, locale_template, control_template, data_template, settings_template
with template_load_lock:
if not data_final_template:
@@ -110,8 +113,6 @@ def generate_mod(world: "Factorio", output_directory: str):
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
versioned_mod_name = mod_name + "_" + Utils.__version__
random = multiworld.per_slot_randoms[player]
def flop_random(low, high, base=None):
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
if base:
@@ -129,43 +130,43 @@ def generate_mod(world: "Factorio", output_directory: str):
"base_tech_table": base_tech_table,
"tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name,
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"custom_technologies": multiworld.worlds[player].custom_technologies,
"allowed_science_packs": world.options.max_science_pack.get_allowed_packs(),
"custom_technologies": world.custom_technologies,
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"slot_name": world.player_name, "seed_name": multiworld.seed_name,
"slot_player": player,
"starting_items": multiworld.starting_items[player], "recipes": recipes,
"starting_items": world.options.starting_items, "recipes": recipes,
"random": random, "flop_random": flop_random,
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
"recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None),
"recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None),
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
"progressive_technology_table": {tech.name: tech.progressive for tech in
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value,
"max_science_pack": world.options.max_science_pack.value,
"liquids": fluids,
"goal": multiworld.goal[player].value,
"energy_link": multiworld.energy_link[player].value,
"goal": world.options.goal.value,
"energy_link": world.options.energy_link.value,
"useless_technologies": useless_technologies,
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
"chunk_shuffle": 0,
}
for factorio_option in Options.factorio_options:
for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items():
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
continue
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
template_data[factorio_option] = factorio_option_instance.value
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
if world.options.silo == Options.Silo.option_randomize_recipe:
template_data["free_sample_blacklist"]["rocket-silo"] = 1
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
if world.options.satellite == Options.Satellite.option_randomize_recipe:
template_data["free_sample_blacklist"]["satellite"] = 1
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value})
template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value})
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
mod = FactorioModFile(zf_path, player=player, player_name=world.player_name)
if world.zip_path:
with zipfile.ZipFile(world.zip_path) as zf:
+40 -50
View File
@@ -1,10 +1,13 @@
from __future__ import annotations
import typing
from dataclasses import dataclass
import datetime
import typing
from schema import Schema, Optional, And, Or
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool
from schema import Schema, Optional, And, Or
StartInventoryPool, PerGameCommonOptions
# schema helpers
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
@@ -422,50 +425,37 @@ class EnergyLink(Toggle):
display_name = "EnergyLink"
factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack,
"goal": Goal,
"tech_tree_layout": TechTreeLayout,
"min_tech_cost": MinTechCost,
"max_tech_cost": MaxTechCost,
"tech_cost_distribution": TechCostDistribution,
"tech_cost_mix": TechCostMix,
"ramping_tech_costs": RampingTechCosts,
"silo": Silo,
"satellite": Satellite,
"free_samples": FreeSamples,
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"free_sample_blacklist": FactorioFreeSampleBlacklist,
"free_sample_whitelist": FactorioFreeSampleWhitelist,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"recipe_ingredients_offset": RecipeIngredientsOffset,
"imported_blueprints": ImportedBlueprint,
"world_gen": FactorioWorldGen,
"progressive": Progressive,
"teleport_traps": TeleportTrapCount,
"grenade_traps": GrenadeTrapCount,
"cluster_grenade_traps": ClusterGrenadeTrapCount,
"artillery_traps": ArtilleryTrapCount,
"atomic_rocket_traps": AtomicRocketTrapCount,
"attack_traps": AttackTrapCount,
"evolution_traps": EvolutionTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
"death_link": DeathLink,
"energy_link": EnergyLink,
"start_inventory_from_pool": StartInventoryPool,
}
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
if datetime.datetime.today().month == 4:
class ChunkShuffle(Toggle):
"""Entrance Randomizer."""
display_name = "Chunk Shuffle"
if datetime.datetime.today().day > 1:
ChunkShuffle.__doc__ += """
2023 April Fool's option. Shuffles chunk border transitions."""
factorio_options["chunk_shuffle"] = ChunkShuffle
@dataclass
class FactorioOptions(PerGameCommonOptions):
max_science_pack: MaxSciencePack
goal: Goal
tech_tree_layout: TechTreeLayout
min_tech_cost: MinTechCost
max_tech_cost: MaxTechCost
tech_cost_distribution: TechCostDistribution
tech_cost_mix: TechCostMix
ramping_tech_costs: RampingTechCosts
silo: Silo
satellite: Satellite
free_samples: FreeSamples
tech_tree_information: TechTreeInformation
starting_items: FactorioStartItems
free_sample_blacklist: FactorioFreeSampleBlacklist
free_sample_whitelist: FactorioFreeSampleWhitelist
recipe_time: RecipeTime
recipe_ingredients: RecipeIngredients
recipe_ingredients_offset: RecipeIngredientsOffset
imported_blueprints: ImportedBlueprint
world_gen: FactorioWorldGen
progressive: Progressive
teleport_traps: TeleportTrapCount
grenade_traps: GrenadeTrapCount
cluster_grenade_traps: ClusterGrenadeTrapCount
artillery_traps: ArtilleryTrapCount
atomic_rocket_traps: AtomicRocketTrapCount
attack_traps: AttackTrapCount
evolution_traps: EvolutionTrapCount
evolution_trap_increase: EvolutionTrapIncrease
death_link: DeathLink
energy_link: EnergyLink
start_inventory_from_pool: StartInventoryPool
+4 -6
View File
@@ -19,12 +19,10 @@ def _sorter(location: "FactorioScienceLocation"):
return location.complexity, location.rel_cost
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
world = factorio_world.multiworld
player = factorio_world.player
def get_shapes(world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
layout = world.tech_tree_layout[player].value
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
layout = world.options.tech_tree_layout.value
locations: List["FactorioScienceLocation"] = sorted(world.science_locations, key=lambda loc: loc.name)
world.random.shuffle(locations)
if layout == TechTreeLayout.option_single:
@@ -247,5 +245,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se
else:
raise NotImplementedError(f"Layout {layout} is not implemented.")
factorio_world.tech_tree_layout_prerequisites = prerequisites
world.tech_tree_layout_prerequisites = prerequisites
return prerequisites
+2 -3
View File
@@ -13,12 +13,11 @@ import Utils
from . import Options
factorio_tech_id = factorio_base_id = 2 ** 17
# Factorio technologies are imported from a .json document in /data
source_folder = os.path.join(os.path.dirname(__file__), "data")
pool = ThreadPoolExecutor(1)
# Factorio technologies are imported from a .json document in /data
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json"))
@@ -99,7 +98,7 @@ class CustomTechnology(Technology):
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
or origin.name == "rocket-silo")
self.player = player
if origin.name not in world.worlds[player].special_nodes:
if origin.name not in world.special_nodes:
if military_allowed:
ingredients.add("military-science-pack")
ingredients = list(ingredients)
+70 -71
View File
@@ -11,7 +11,7 @@ from worlds.LauncherComponents import Component, components, Type, launch_subpro
from worlds.generic import Rules
from .Locations import location_pools, location_table
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
from .Shapes import get_shapes
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
@@ -89,13 +89,15 @@ class Factorio(World):
advancement_technologies: typing.Set[str]
web = FactorioWeb()
options_dataclass = FactorioOptions
options: FactorioOptions
item_name_to_id = all_items
location_name_to_id = location_table
item_name_groups = {
"Progressive": set(progressive_tech_table.keys()),
}
required_client_version = (0, 4, 2)
required_client_version = (0, 5, 0)
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
@@ -117,32 +119,32 @@ class Factorio(World):
def generate_early(self) -> None:
# if max < min, then swap max and min
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
if self.options.max_tech_cost < self.options.min_tech_cost:
self.options.min_tech_cost.value, self.options.max_tech_cost.value = \
self.options.max_tech_cost.value, self.options.min_tech_cost.value
self.tech_mix = self.options.tech_cost_mix.value
self.skip_silo = self.options.silo.value == Silo.option_spawn
def create_regions(self):
player = self.player
random = self.multiworld.random
random = self.random
nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.multiworld.evolution_traps[player] + \
self.multiworld.attack_traps[player] + \
self.multiworld.teleport_traps[player] + \
self.multiworld.grenade_traps[player] + \
self.multiworld.cluster_grenade_traps[player] + \
self.multiworld.atomic_rocket_traps[player] + \
self.multiworld.artillery_traps[player]
self.options.evolution_traps + \
self.options.attack_traps + \
self.options.teleport_traps + \
self.options.grenade_traps + \
self.options.cluster_grenade_traps + \
self.options.atomic_rocket_traps + \
self.options.artillery_traps
location_pool = []
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
location_pool.extend(location_pools[pack])
try:
location_names = self.multiworld.random.sample(location_pool, location_count)
location_names = random.sample(location_pool, location_count)
except ValueError as e:
# should be "ValueError: Sample larger than population or is negative"
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
@@ -150,9 +152,9 @@ class Factorio(World):
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names]
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
min_cost = self.multiworld.min_tech_cost[self.player]
max_cost = self.multiworld.max_tech_cost[self.player]
distribution: TechCostDistribution = self.options.tech_cost_distribution
min_cost = self.options.min_tech_cost.value
max_cost = self.options.max_tech_cost.value
if distribution == distribution.option_even:
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
else:
@@ -161,7 +163,7 @@ class Factorio(World):
distribution.option_high: max_cost}[distribution.value]
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
rand_values = sorted(rand_values)
if self.multiworld.ramping_tech_costs[self.player]:
if self.options.ramping_tech_costs:
def sorter(loc: FactorioScienceLocation):
return loc.complexity, loc.rel_cost
else:
@@ -176,7 +178,7 @@ class Factorio(World):
event = FactorioItem("Victory", ItemClassification.progression, None, player)
location.place_locked_item(event)
for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()):
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
nauvis.locations.append(location)
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
@@ -185,24 +187,23 @@ class Factorio(World):
self.multiworld.regions.append(nauvis)
def create_items(self) -> None:
player = self.player
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.multiworld,
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
range(getattr(self.options,
f"{trap_name.lower().replace(' ', '_')}_traps")))
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
want_progressives(self.multiworld.random))
want_progressives = collections.defaultdict(lambda: self.options.progressive.
want_progressives(self.random))
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
special_index = {"automation": 0,
"logistics": 1,
"rocket-silo": -1}
loc: FactorioScienceLocation
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full:
if self.options.tech_tree_information == TechTreeInformation.option_full:
# mark all locations as pre-hinted
for loc in self.science_locations:
loc.revealed = True
@@ -229,14 +230,13 @@ class Factorio(World):
loc.revealed = True
def set_rules(self):
world = self.multiworld
player = self.player
shapes = get_shapes(self)
for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player)
for ingredient in self.options.max_science_pack.get_allowed_packs():
location = self.get_location(f"Automate {ingredient}")
if self.multiworld.recipe_ingredients[self.player]:
if self.options.recipe_ingredients:
custom_recipe = self.custom_recipes[ingredient]
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
@@ -257,30 +257,30 @@ class Factorio(World):
prerequisites: all(state.can_reach(loc) for loc in locations))
silo_recipe = None
if self.multiworld.silo[self.player] == Silo.option_spawn:
if self.options.silo == Silo.option_spawn:
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None
if self.multiworld.goal[self.player] == Goal.option_satellite:
if self.options.goal == Goal.option_satellite:
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
else next(iter(all_product_sources.get("satellite")))
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
if self.multiworld.silo[self.player] != Silo.option_spawn:
if self.options.silo != Silo.option_spawn:
victory_tech_names.add("rocket-silo")
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
world.completion_condition[player] = lambda state: state.has('Victory', player)
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
def generate_basic(self):
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
map_basic_settings = self.options.world_gen.value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0
# 32 bit uint
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)
map_basic_settings["seed"] = self.random.randint(0, 2 ** 32 - 1)
start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value
start_location_hints: typing.Set[str] = self.options.start_location_hints.value
for loc in self.science_locations:
# show start_location_hints ingame
@@ -304,8 +304,6 @@ class Factorio(World):
return super(Factorio, self).collect_item(state, item, remove)
option_definitions = factorio_options
@classmethod
def stage_write_spoiler(cls, world, spoiler_handle):
factorio_players = world.get_game_players(cls.game)
@@ -345,7 +343,7 @@ class Factorio(World):
# have to first sort for determinism, while filtering out non-stacking items
pool: typing.List[str] = sorted(pool & valid_ingredients)
# then sort with random data to shuffle
self.multiworld.random.shuffle(pool)
self.random.shuffle(pool)
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
target_energy = original.total_energy * factor
target_num_ingredients = len(original.ingredients) + ingredients_offset
@@ -389,7 +387,7 @@ class Factorio(World):
if min_num > max_num:
fallback_pool.append(ingredient)
continue # can't use that ingredient
num = self.multiworld.random.randint(min_num, max_num)
num = self.random.randint(min_num, max_num)
new_ingredients[ingredient] = num
remaining_raw -= num * ingredient_raw
remaining_energy -= num * ingredient_energy
@@ -433,66 +431,66 @@ class Factorio(World):
def set_custom_technologies(self):
custom_technologies = {}
allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs()
allowed_packs = self.options.max_science_pack.get_allowed_packs()
for technology_name, technology in base_technology_table.items():
custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player)
custom_technologies[technology_name] = technology.get_custom(self, allowed_packs, self.player)
return custom_technologies
def set_custom_recipes(self):
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
ingredients_offset = self.options.recipe_ingredients_offset
original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
self.multiworld.random.shuffle(valid_pool)
valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients)
self.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
original_rocket_part.products,
original_rocket_part.energy)}
if self.multiworld.recipe_ingredients[self.player]:
if self.options.recipe_ingredients:
valid_pool = []
for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs():
for pack in self.options.max_science_pack.get_ordered_science_packs():
valid_pool += sorted(science_pack_pools[pack])
self.multiworld.random.shuffle(valid_pool)
self.random.shuffle(valid_pool)
if pack in recipes: # skips over space science pack
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset=
ingredients_offset)
ingredients_offset.value)
self.custom_recipes[pack] = new_recipe
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
if self.options.silo.value == Silo.option_randomize_recipe \
or self.options.satellite.value == Satellite.option_randomize_recipe:
valid_pool = set()
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
valid_pool |= science_pack_pools[pack]
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
if self.options.silo.value == Silo.option_randomize_recipe:
new_recipe = self.make_balanced_recipe(
recipes["rocket-silo"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
ingredients_offset=ingredients_offset)
factor=(self.options.max_science_pack.value + 1) / 7,
ingredients_offset=ingredients_offset.value)
self.custom_recipes["rocket-silo"] = new_recipe
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
if self.options.satellite.value == Satellite.option_randomize_recipe:
new_recipe = self.make_balanced_recipe(
recipes["satellite"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
ingredients_offset=ingredients_offset)
factor=(self.options.max_science_pack.value + 1) / 7,
ingredients_offset=ingredients_offset.value)
self.custom_recipes["satellite"] = new_recipe
bridge = "ap-energy-bridge"
new_recipe = self.make_quick_recipe(
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
"replace_4": 1, "replace_5": 1, "replace_6": 1},
{bridge: 1}, 10),
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
ingredients_offset=ingredients_offset)
sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]),
ingredients_offset=ingredients_offset.value)
for ingredient_name in new_recipe.ingredients:
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
new_recipe.ingredients[ingredient_name] = self.random.randint(50, 500)
self.custom_recipes[bridge] = new_recipe
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
if self.multiworld.silo[self.player] != Silo.option_spawn:
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
if self.options.silo != Silo.option_spawn:
needed_recipes |= {"rocket-silo"}
if self.multiworld.goal[self.player].value == Goal.option_satellite:
if self.options.goal.value == Goal.option_satellite:
needed_recipes |= {"satellite"}
for recipe in needed_recipes:
@@ -542,7 +540,8 @@ class FactorioScienceLocation(FactorioLocation):
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
for complexity in range(self.complexity):
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
if (parent.multiworld.worlds[self.player].options.tech_cost_mix >
parent.multiworld.worlds[self.player].random.randint(0, 99)):
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
@property
+2 -2
View File
@@ -131,8 +131,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
the location without using any hint points.
* `start_location_hints` is the same as `start_hints` but for locations, allowing you to hint for the item contained
there without using any hint points.
* `exclude_locations` lets you define any locations that you don't want to do and prevents items classified as
"progression" or "useful" from being placed on them.
* `exclude_locations` lets you define any locations that you don't want to do and forces a filler or trap item which
isn't necessary for progression into these locations.
* `priority_locations` lets you define any locations that you want to do and forces a progression item into these
locations.
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared
+26 -2
View File
@@ -21,6 +21,16 @@ from .Charms import names as charm_names
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
from worlds.AutoWorld import World, LogicMixin, WebWorld
from settings import Group, Bool
class HollowKnightSettings(Group):
class DisableMapModSpoilers(Bool):
"""Disallows the APMapMod from showing spoiler placements."""
disable_spoilers: typing.Union[DisableMapModSpoilers, bool] = False
path_of_pain_locations = {
"Soul_Totem-Path_of_Pain_Below_Thornskip",
"Lore_Tablet-Path_of_Pain_Entrance",
@@ -124,14 +134,25 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
class HKWeb(WebWorld):
tutorials = [Tutorial(
setup_en = Tutorial(
"Mod Setup and Use Guide",
"A guide to playing Hollow Knight with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Ijwu"]
)]
)
setup_pt_br = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Português Brasileiro",
"setup_pt_br.md",
"setup/pt_br",
["JoaoVictor-FA"]
)
tutorials = [setup_en, setup_pt_br]
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
@@ -145,6 +166,7 @@ class HKWorld(World):
game: str = "Hollow Knight"
options_dataclass = HKOptions
options: HKOptions
settings: typing.ClassVar[HollowKnightSettings]
web = HKWeb()
@@ -544,6 +566,8 @@ class HKWorld(World):
slot_data["grub_count"] = self.grub_count
slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race)
return slot_data
def create_item(self, name: str) -> HKItem:
+1 -1
View File
@@ -15,7 +15,7 @@
### What to do if Lumafly fails to find your installation directory
1. Find the directory manually.
* Xbox Game Pass:
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar.
2. Click the three points then click "Manage".
3. Go to the "Files" tab and select "Browse...".
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
+52
View File
@@ -0,0 +1,52 @@
# Guia de configuração para Hollow Knight no Archipelago
## Programas obrigatórios
* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/).
* Uma cópia legal de Hollow Knight.
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
## Instalando o mod Archipelago Mod usando Lumafly
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.
2. Clique em "Install (instalar)" perto da opção "Archipelago" mod.
* Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
3. Abra o jogo, tudo preparado!
### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação
1. Encontre a pasta manualmente.
* Xbox Game Pass:
1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
2. Clique nos 3 pontos depois clique gerenciar.
3. Vá nos arquivos e selecione procurar.
4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
* Steam:
1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
. Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
* Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
* Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou.
## Configurando seu arquivo YAML
### O que é um YAML e por que eu preciso de um?
Um arquivo YAML é a forma que você informa suas configurações do jogador para o Archipelago.
Olhe o [guia de configuração básica de um multiworld](/tutorial/Archipelago/setup/en) aqui no site do Archipelago para aprender mais.
### Onde eu consigo o YAML?
Você pode usar a [página de configurações do jogador para Hollow Knight](/games/Hollow%20Knight/player-options) aqui no site do Archipelago
para gerar o YAML usando a interface gráfica.
### Entrando numa partida de Archipelago no Hollow Knight
1. Começe o jogo depois de instalar todos os mods necessários.
2. Crie um **novo jogo salvo.**
3. Selecione o modo de jogo **Archipelago** do menu de seleção.
4. Coloque as configurações corretas do seu servidor Archipelago.
5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens.
6. O jogo vai te colocar imediatamente numa partida randomizada.
* Se você está esperando uma contagem então espere ele cair antes de apertar começar.
* Ou clique em começar e pause o jogo enquanto estiver nele.
## Dicas e outros comandos
Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no
[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso,
que está incluido na ultima versão do [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
+47 -5
View File
@@ -31,6 +31,9 @@ def check_stdin() -> None:
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
class KH1ClientCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_deathlink(self):
"""Toggles Deathlink"""
global death_link
@@ -40,6 +43,40 @@ class KH1ClientCommandProcessor(ClientCommandProcessor):
else:
death_link = True
self.output(f"Death Link turned on")
def _cmd_goal(self):
"""Prints goal setting"""
if "goal" in self.ctx.slot_data.keys():
self.output(str(self.ctx.slot_data["goal"]))
else:
self.output("Unknown")
def _cmd_eotw_unlock(self):
"""Prints End of the World Unlock setting"""
if "required_reports_door" in self.ctx.slot_data.keys():
if self.ctx.slot_data["required_reports_door"] > 13:
self.output("Item")
else:
self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports")
else:
self.output("Unknown")
def _cmd_door_unlock(self):
"""Prints Final Rest Door Unlock setting"""
if "door" in self.ctx.slot_data.keys():
if self.ctx.slot_data["door"] == "reports":
self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports")
else:
self.output(str(self.ctx.slot_data["door"]))
else:
self.output("Unknown")
def _cmd_advanced_logic(self):
"""Prints advanced logic setting"""
if "advanced_logic" in self.ctx.slot_data.keys():
self.output(str(self.ctx.slot_data["advanced_logic"]))
else:
self.output("Unknown")
class KH1Context(CommonContext):
command_processor: int = KH1ClientCommandProcessor
@@ -51,6 +88,8 @@ class KH1Context(CommonContext):
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
self.hinted_synth_location_ids = False
self.slot_data = {}
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM")
@@ -104,6 +143,7 @@ class KH1Context(CommonContext):
f.close()
#Handle Slot Data
self.slot_data = args['slot_data']
for key in list(args['slot_data'].keys()):
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
f.write(str(args['slot_data'][key]))
@@ -217,11 +257,13 @@ async def game_watcher(ctx: KH1Context):
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
await ctx.send_death(death_text = "Sora was defeated!")
if file.find("insynthshop") > -1:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
"create_as_hint": 2
}])
if not ctx.hinted_synth_location_ids:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
"create_as_hint": 2
}])
ctx.hinted_synth_location_ids = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
+2 -2
View File
@@ -83,8 +83,8 @@ class ItemName:
RUPEES_200 = "200 Rupees"
RUPEES_500 = "500 Rupees"
SEASHELL = "Seashell"
MESSAGE = "Master Stalfos' Message"
GEL = "Gel"
MESSAGE = "Nothing"
GEL = "Zol Attack"
BOOMERANG = "Boomerang"
HEART_PIECE = "Heart Piece"
BOWWOW = "BowWow"
+2 -1
View File
@@ -29,6 +29,7 @@ def fixGoldenLeaf(rom):
rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves
rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path
rom.patch(0x06, 0x00B6, ASM("ld a, $FF"), ASM("ld a, $06"))
rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores 6 in the leaf counter if we opened the path (instead of FF, so that nothing breaks if we get more for some reason)
# 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message.
# rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler
+17 -1
View File
@@ -149,6 +149,8 @@ class MagpieBridge:
item_tracker = None
ws = None
features = []
slot_data = {}
async def handler(self, websocket):
self.ws = websocket
while True:
@@ -163,6 +165,9 @@ class MagpieBridge:
await self.send_all_inventory()
if "checks" in self.features:
await self.send_all_checks()
if "slot_data" in self.features:
await self.send_slot_data(self.slot_data)
# Translate renamed IDs back to LADXR IDs
@staticmethod
def fixup_id(the_id):
@@ -222,6 +227,18 @@ class MagpieBridge:
return
await gps.send_location(self.ws)
async def send_slot_data(self, slot_data):
if not self.ws:
return
logger.debug("Sending slot_data to magpie.")
message = {
"type": "slot_data",
"slot_data": slot_data
}
await self.ws.send(json.dumps(message))
async def serve(self):
async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger):
await asyncio.Future() # run forever
@@ -237,4 +254,3 @@ class MagpieBridge:
await self.send_all_inventory()
else:
await self.send_inventory_diffs()
+32 -1
View File
@@ -216,7 +216,7 @@ class LinksAwakeningWorld(World):
for _ in range(count):
if item_name in exclude:
exclude.remove(item_name) # this is destructive. create unique list above
self.multiworld.itempool.append(self.create_item("Master Stalfos' Message"))
self.multiworld.itempool.append(self.create_item("Nothing"))
else:
item = self.create_item(item_name)
@@ -512,3 +512,34 @@ class LinksAwakeningWorld(World):
if change and item.name in self.rupees:
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
return change
def get_filler_item_name(self) -> str:
return "Nothing"
def fill_slot_data(self):
slot_data = {}
if not self.multiworld.is_race:
# all of these option are NOT used by the LADX- or Text-Client.
# they are used by Magpie tracker (https://github.com/kbranch/Magpie/wiki/Autotracker-API)
# for convenient auto-tracking of the generated settings and adjusting the tracker accordingly
slot_options = ["instrument_count"]
slot_options_display_name = [
"goal", "logic", "tradequest", "rooster",
"experimental_dungeon_shuffle", "experimental_entrance_shuffle", "trendy_game", "gfxmod",
"shuffle_nightmare_keys", "shuffle_small_keys", "shuffle_maps",
"shuffle_compasses", "shuffle_stone_beaks", "shuffle_instruments", "nag_messages"
]
# use the default behaviour to grab options
slot_data = self.options.as_dict(*slot_options)
# for options which should not get the internal int value but the display name use the extra handling
slot_data.update({
option: value.current_key
for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name
})
return slot_data
+3 -1
View File
@@ -482,7 +482,9 @@
Crossroads:
door: Crossroads Entrance
The Tenacious:
door: Tenacious Entrance
- door: Tenacious Entrance
- room: The Tenacious
door: Shortcut to Hub Room
Near Far Area: True
Hedge Maze:
door: Shortcut to Hedge Maze
Binary file not shown.
+23 -10
View File
@@ -215,13 +215,13 @@ def shuffle_portals(world: "MessengerWorld") -> None:
if "Portal" in warp:
exit_string += "Portal"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00"))
world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}00"))
elif warp in SHOP_POINTS[parent]:
exit_string += f"{warp} Shop"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}"))
world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}"))
else:
exit_string += f"{warp} Checkpoint"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}"))
world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}"))
world.spoiler_portal_mapping[in_portal] = exit_string
connect_portal(world, in_portal, exit_string)
@@ -230,12 +230,15 @@ def shuffle_portals(world: "MessengerWorld") -> None:
def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
"""checks the provided plando connections for portals and connects them"""
nonlocal available_portals
for connection in plando_connections:
if connection.entrance not in PORTALS:
continue
# let it crash here if input is invalid
create_mapping(connection.entrance, connection.exit)
available_portals.remove(connection.exit)
parent = create_mapping(connection.entrance, connection.exit)
world.plando_portals.append(connection.entrance)
if shuffle_type < ShufflePortals.option_anywhere:
available_portals = [port for port in available_portals if port not in shop_points[parent]]
shuffle_type = world.options.shuffle_portals
shop_points = deepcopy(SHOP_POINTS)
@@ -251,8 +254,13 @@ def shuffle_portals(world: "MessengerWorld") -> None:
plando = world.options.portal_plando.value
if not plando:
plando = world.options.plando_connections.value
if plando and world.multiworld.plando_options & PlandoOptions.connections:
handle_planned_portals(plando)
if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals:
try:
handle_planned_portals(plando)
# any failure i expect will trigger on available_portals.remove
except ValueError:
raise ValueError(f"Unable to complete portal plando for Player {world.player_name}. "
f"If you attempted to plando a checkpoint, checkpoints must be shuffled.")
for portal in PORTALS:
if portal in world.plando_portals:
@@ -276,8 +284,13 @@ def disconnect_portals(world: "MessengerWorld") -> None:
entrance.connected_region = None
if portal in world.spoiler_portal_mapping:
del world.spoiler_portal_mapping[portal]
if len(world.portal_mapping) > len(world.spoiler_portal_mapping):
world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)]
if world.plando_portals:
indexes = [PORTALS.index(portal) for portal in world.plando_portals]
planned_portals = []
for index, portal_coord in enumerate(world.portal_mapping):
if index in indexes:
planned_portals.append(portal_coord)
world.portal_mapping = planned_portals
def validate_portals(world: "MessengerWorld") -> bool:
+19 -6
View File
@@ -85,7 +85,7 @@ class MLSSClient(BizHawkClient):
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:
if seed not in 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,"
@@ -143,17 +143,30 @@ class MLSSClient(BizHawkClient):
# 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:
result = False
total = 0
while not result:
await asyncio.sleep(0.05)
total += 0.05
result = await bizhawk.guarded_write(
ctx.bizhawk_ctx,
[
(0x3057, [id_to_RAM(item_data.itemID)], "EWRAM")
],
[(0x3057, [0x0], "EWRAM")]
)
if result:
total = 0
if total >= 1:
break
if not result:
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
+4 -2
View File
@@ -1,6 +1,9 @@
flying = [
0x14,
0x1D,
0x32,
0x33,
0x40,
0x4C
]
@@ -23,7 +26,6 @@ enemies = [
0x5032AC,
0x5032CC,
0x5032EC,
0x50330C,
0x50332C,
0x50334C,
0x50336C,
@@ -151,7 +153,7 @@ enemies = [
0x50458C,
0x5045AC,
0x50468C,
0x5046CC,
# 0x5046CC, 6 enemy formation
0x5046EC,
0x50470C
]
+15 -15
View File
@@ -78,21 +78,21 @@ itemList: typing.List[ItemData] = [
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(77771063, "Beanlet 1", ItemClassification.useful, 0x73),
ItemData(77771064, "Beanlet 2", ItemClassification.useful, 0x74),
ItemData(77771065, "Beanlet 3", ItemClassification.useful, 0x75),
ItemData(77771066, "Beanlet 4", ItemClassification.useful, 0x76),
ItemData(77771067, "Beanlet 5", ItemClassification.useful, 0x77),
ItemData(77771068, "Beanstone 1", ItemClassification.useful, 0x80),
ItemData(77771069, "Beanstone 2", ItemClassification.useful, 0x81),
ItemData(77771070, "Beanstone 3", ItemClassification.useful, 0x82),
ItemData(77771071, "Beanstone 4", ItemClassification.useful, 0x83),
ItemData(77771072, "Beanstone 5", ItemClassification.useful, 0x84),
ItemData(77771073, "Beanstone 6", ItemClassification.useful, 0x85),
ItemData(77771074, "Beanstone 7", ItemClassification.useful, 0x86),
ItemData(77771075, "Beanstone 8", ItemClassification.useful, 0x87),
ItemData(77771076, "Beanstone 9", ItemClassification.useful, 0x90),
ItemData(77771077, "Beanstone 10", ItemClassification.useful, 0x91),
ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92),
ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93),
ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F),
+59 -58
View File
@@ -4,9 +4,6 @@ from BaseClasses import Location
class LocationData:
name: str = ""
id: int = 0x00
def __init__(self, name, id_, itemType):
self.name = name
self.itemType = itemType
@@ -93,8 +90,8 @@ mainArea: typing.List[LocationData] = [
LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0),
LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0),
LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0),
LocationData("Hoohoo Mountain After Hoohooros Block 1", 0x39D890, 0),
LocationData("Hoohoo Mountain After Hoohooros Block 2", 0x39D8A0, 0),
LocationData("Hoohoo Mountain Past Hoohooros Block 1", 0x39D890, 0),
LocationData("Hoohoo Mountain Past Hoohooros Block 2", 0x39D8A0, 0),
LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0),
LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0),
LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0),
@@ -104,7 +101,7 @@ mainArea: typing.List[LocationData] = [
LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0),
LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0),
LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0),
LocationData("Hoohoo Village Right Side Block", 0x39D957, 0),
LocationData("Hoohoo Village Eastside Block", 0x39D957, 0),
LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0),
LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0),
LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0),
@@ -119,8 +116,8 @@ mainArea: typing.List[LocationData] = [
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0),
LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0),
LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0),
LocationData("Hoohoo Mountain Base After Minecart Minigame Block 1", 0x39DA35, 0),
LocationData("Hoohoo Mountain Base After Minecart Minigame Block 2", 0x39DA2D, 0),
LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 1", 0x39DA35, 0),
LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 2", 0x39DA2D, 0),
LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0),
LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0),
LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0),
@@ -143,14 +140,14 @@ mainArea: typing.List[LocationData] = [
LocationData("Shop Starting Flag 3", 0x3C05F4, 3),
LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0),
LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0),
LocationData("Hoohoo Mountain After Hoohooros Digspot", 0x39D898, 0),
LocationData("Hoohoo Mountain Past Hoohooros Digspot", 0x39D898, 0),
LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0),
LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0),
LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0),
LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0),
LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0),
LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0),
LocationData("Hoohoo Village Right Side Digspot", 0x39D95F, 0),
LocationData("Hoohoo Village Eastside Digspot", 0x39D95F, 0),
LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0),
LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0),
LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0),
@@ -267,7 +264,7 @@ coins: typing.List[LocationData] = [
LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0),
LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0),
LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0),
LocationData("Chucklehuck Woods After Chuckleroot Coin Block", 0x39DF14, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0),
LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0),
LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0),
LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0),
@@ -286,11 +283,12 @@ baseUltraRocks: typing.List[LocationData] = [
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0),
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0),
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0),
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Rightside)", 0x39D9E9, 0),
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Right Side)", 0x39D9E9, 0),
LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1),
LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0),
LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 1", 0x39E5C8, 0),
LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 2", 0x39E5D0, 0),
LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0),
LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0),
LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0),
LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0),
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0),
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0),
@@ -345,12 +343,12 @@ chucklehuck: typing.List[LocationData] = [
LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0),
LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0),
LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0),
LocationData("Chucklehuck Woods After Chuckleroot Block 1", 0x39DEE4, 0),
LocationData("Chucklehuck Woods After Chuckleroot Block 2", 0x39DEEC, 0),
LocationData("Chucklehuck Woods After Chuckleroot Block 3", 0x39DEF4, 0),
LocationData("Chucklehuck Woods After Chuckleroot Block 4", 0x39DEFC, 0),
LocationData("Chucklehuck Woods After Chuckleroot Block 5", 0x39DF04, 0),
LocationData("Chucklehuck Woods After Chuckleroot Block 6", 0x39DF0C, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Block 1", 0x39DEE4, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Block 2", 0x39DEEC, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Block 3", 0x39DEF4, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Block 4", 0x39DEFC, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Block 5", 0x39DF04, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Block 6", 0x39DF0C, 0),
LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0),
LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0),
LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0),
@@ -367,14 +365,14 @@ chucklehuck: typing.List[LocationData] = [
]
castleTown: typing.List[LocationData] = [
LocationData("Beanbean Castle Town Left Side House Block 1", 0x39D7A4, 0),
LocationData("Beanbean Castle Town Left Side House Block 2", 0x39D7AC, 0),
LocationData("Beanbean Castle Town Left Side House Block 3", 0x39D7B4, 0),
LocationData("Beanbean Castle Town Left Side House Block 4", 0x39D7BC, 0),
LocationData("Beanbean Castle Town Right Side House Block 1", 0x39D7D8, 0),
LocationData("Beanbean Castle Town Right Side House Block 2", 0x39D7E0, 0),
LocationData("Beanbean Castle Town Right Side House Block 3", 0x39D7E8, 0),
LocationData("Beanbean Castle Town Right Side House Block 4", 0x39D7F0, 0),
LocationData("Beanbean Castle Town West Side House Block 1", 0x39D7A4, 0),
LocationData("Beanbean Castle Town West Side House Block 2", 0x39D7AC, 0),
LocationData("Beanbean Castle Town West Side House Block 3", 0x39D7B4, 0),
LocationData("Beanbean Castle Town West Side House Block 4", 0x39D7BC, 0),
LocationData("Beanbean Castle Town East Side House Block 1", 0x39D7D8, 0),
LocationData("Beanbean Castle Town East Side House Block 2", 0x39D7E0, 0),
LocationData("Beanbean Castle Town East Side House Block 3", 0x39D7E8, 0),
LocationData("Beanbean Castle Town East Side House Block 4", 0x39D7F0, 0),
LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2),
LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2),
LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1),
@@ -444,14 +442,14 @@ piranhaFlag: typing.List[LocationData] = [
]
kidnappedFlag: typing.List[LocationData] = [
LocationData("Badge Shop Enter Fungitown Flag 1", 0x3C0640, 2),
LocationData("Badge Shop Enter Fungitown Flag 2", 0x3C0642, 2),
LocationData("Badge Shop Enter Fungitown Flag 3", 0x3C0644, 2),
LocationData("Pants Shop Enter Fungitown Flag 1", 0x3C0646, 2),
LocationData("Pants Shop Enter Fungitown Flag 2", 0x3C0648, 2),
LocationData("Pants Shop Enter Fungitown Flag 3", 0x3C064A, 2),
LocationData("Shop Enter Fungitown Flag 1", 0x3C0606, 3),
LocationData("Shop Enter Fungitown Flag 2", 0x3C0608, 3),
LocationData("Badge Shop Trunkle Flag 1", 0x3C0640, 2),
LocationData("Badge Shop Trunkle Flag 2", 0x3C0642, 2),
LocationData("Badge Shop Trunkle Flag 3", 0x3C0644, 2),
LocationData("Pants Shop Trunkle Flag 1", 0x3C0646, 2),
LocationData("Pants Shop Trunkle Flag 2", 0x3C0648, 2),
LocationData("Pants Shop Trunkle Flag 3", 0x3C064A, 2),
LocationData("Shop Trunkle Flag 1", 0x3C0606, 3),
LocationData("Shop Trunkle Flag 2", 0x3C0608, 3),
]
beanstarFlag: typing.List[LocationData] = [
@@ -553,21 +551,21 @@ surfable: typing.List[LocationData] = [
airport: typing.List[LocationData] = [
LocationData("Airport Entrance Digspot", 0x39E2DC, 0),
LocationData("Airport Lobby Digspot", 0x39E2E9, 0),
LocationData("Airport Leftside Digspot 1", 0x39E2F6, 0),
LocationData("Airport Leftside Digspot 2", 0x39E2FE, 0),
LocationData("Airport Leftside Digspot 3", 0x39E306, 0),
LocationData("Airport Leftside Digspot 4", 0x39E30E, 0),
LocationData("Airport Leftside Digspot 5", 0x39E316, 0),
LocationData("Airport Westside Digspot 1", 0x39E2F6, 0),
LocationData("Airport Westside Digspot 2", 0x39E2FE, 0),
LocationData("Airport Westside Digspot 3", 0x39E306, 0),
LocationData("Airport Westside Digspot 4", 0x39E30E, 0),
LocationData("Airport Westside Digspot 5", 0x39E316, 0),
LocationData("Airport Center Digspot 1", 0x39E323, 0),
LocationData("Airport Center Digspot 2", 0x39E32B, 0),
LocationData("Airport Center Digspot 3", 0x39E333, 0),
LocationData("Airport Center Digspot 4", 0x39E33B, 0),
LocationData("Airport Center Digspot 5", 0x39E343, 0),
LocationData("Airport Rightside Digspot 1", 0x39E350, 0),
LocationData("Airport Rightside Digspot 2", 0x39E358, 0),
LocationData("Airport Rightside Digspot 3", 0x39E360, 0),
LocationData("Airport Rightside Digspot 4", 0x39E368, 0),
LocationData("Airport Rightside Digspot 5", 0x39E370, 0),
LocationData("Airport Eastside Digspot 1", 0x39E350, 0),
LocationData("Airport Eastside Digspot 2", 0x39E358, 0),
LocationData("Airport Eastside Digspot 3", 0x39E360, 0),
LocationData("Airport Eastside Digspot 4", 0x39E368, 0),
LocationData("Airport Eastside Digspot 5", 0x39E370, 0),
]
gwarharEntrance: typing.List[LocationData] = [
@@ -617,7 +615,6 @@ teeheeValley: typing.List[LocationData] = [
LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0),
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0),
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0),
LocationData("Teehee Valley Solo Luigi Maze Room 1 Block", 0x39E5E0, 0),
LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0),
LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0),
LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0),
@@ -667,7 +664,7 @@ bowsers: typing.List[LocationData] = [
LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0),
LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0),
LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0),
LocationData("Bowser's Castle After Morton Block", 0x39EA0C, 0),
LocationData("Bowser's Castle Past Morton Block", 0x39EA0C, 0),
LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0),
LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0),
LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0),
@@ -705,16 +702,16 @@ jokesEntrance: typing.List[LocationData] = [
LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0),
LocationData("Joke's End Mole Reward 1", 0x27788E, 1),
LocationData("Joke's End Mole Reward 2", 0x2778D2, 1),
]
jokesMain: typing.List[LocationData] = [
LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0),
LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0),
LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0),
LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0),
LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0),
LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0),
LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0),
LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0),
]
jokesMain: typing.List[LocationData] = [
LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0),
LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0),
LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0),
@@ -740,10 +737,10 @@ jokesMain: typing.List[LocationData] = [
postJokes: typing.List[LocationData] = [
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0),
LocationData("Teehee Valley Before Popple Digspot 1", 0x39E55B, 0),
LocationData("Teehee Valley Before Popple Digspot 2", 0x39E563, 0),
LocationData("Teehee Valley Before Popple Digspot 3", 0x39E56B, 0),
LocationData("Teehee Valley Before Popple Digspot 4", 0x39E573, 0),
LocationData("Teehee Valley Before Birdo Digspot 1", 0x39E55B, 0),
LocationData("Teehee Valley Before Birdo Digspot 2", 0x39E563, 0),
LocationData("Teehee Valley Before Birdo Digspot 3", 0x39E56B, 0),
LocationData("Teehee Valley Before Birdo Digspot 4", 0x39E573, 0),
]
theater: typing.List[LocationData] = [
@@ -766,6 +763,10 @@ oasis: typing.List[LocationData] = [
LocationData("Oho Oasis Thunderhand", 0x1E9409, 2),
]
cacklettas_soul: typing.List[LocationData] = [
LocationData("Cackletta's Soul", None, 0),
]
nonBlock = [
(0x434B, 0x1, 0x243844), # Farm Mole 1
(0x434B, 0x1, 0x24387D), # Farm Mole 2
@@ -1171,15 +1172,15 @@ all_locations: typing.List[LocationData] = (
+ fungitownBeanstar
+ fungitownBirdo
+ bowsers
+ bowsersMini
+ jokesEntrance
+ jokesMain
+ postJokes
+ theater
+ oasis
+ gwarharMain
+ bowsersMini
+ baseUltraRocks
+ coins
)
location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations}
location_table: typing.Dict[str, int] = {location.name: location.id for location in all_locations}
+44 -44
View File
@@ -8,14 +8,14 @@ class LocationName:
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"
BeanbeanCastleTownWestsideHouseBlock1 = "Beanbean Castle Town Westside House Block 1"
BeanbeanCastleTownWestsideHouseBlock2 = "Beanbean Castle Town Westside House Block 2"
BeanbeanCastleTownWestsideHouseBlock3 = "Beanbean Castle Town Westside House Block 3"
BeanbeanCastleTownWestsideHouseBlock4 = "Beanbean Castle Town Westside House Block 4"
BeanbeanCastleTownEastsideHouseBlock1 = "Beanbean Castle Town Eastside House Block 1"
BeanbeanCastleTownEastsideHouseBlock2 = "Beanbean Castle Town Eastside House Block 2"
BeanbeanCastleTownEastsideHouseBlock3 = "Beanbean Castle Town Eastside House Block 3"
BeanbeanCastleTownEastsideHouseBlock4 = "Beanbean Castle Town Eastside 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"
@@ -26,9 +26,9 @@ class LocationName:
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"
HoohooMountainPastHoohoorosBlock1 = "Hoohoo Mountain Past Hoohooros Block 1"
HoohooMountainPastHoohoorosDigspot = "Hoohoo Mountain Past Hoohooros Digspot"
HoohooMountainPastHoohoorosBlock2 = "Hoohoo Mountain Past Hoohooros Block 2"
HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1"
HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2"
HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1"
@@ -44,8 +44,8 @@ class LocationName:
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"
HoohooVillageEastsideBlock = "Hoohoo Village Eastside Block"
HoohooVillageEastsideDigspot = "Hoohoo Village Eastside Digspot"
HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1"
HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2"
HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3"
@@ -65,8 +65,8 @@ class LocationName:
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"
HoohooMountainBasePastMinecartMinigameBlock1 = "Hoohoo Mountain Base Past Minecart Minigame Block 1"
HoohooMountainBasePastMinecartMinigameBlock2 = "Hoohoo Mountain Base Past 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"
@@ -148,12 +148,12 @@ class LocationName:
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"
ChucklehuckWoodsPastChucklerootBlock1 = "Chucklehuck Woods Past Chuckleroot Block 1"
ChucklehuckWoodsPastChucklerootBlock2 = "Chucklehuck Woods Past Chuckleroot Block 2"
ChucklehuckWoodsPastChucklerootBlock3 = "Chucklehuck Woods Past Chuckleroot Block 3"
ChucklehuckWoodsPastChucklerootBlock4 = "Chucklehuck Woods Past Chuckleroot Block 4"
ChucklehuckWoodsPastChucklerootBlock5 = "Chucklehuck Woods Past Chuckleroot Block 5"
ChucklehuckWoodsPastChucklerootBlock6 = "Chucklehuck Woods Past Chuckleroot Block 6"
WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block"
WinkleAreaDigspot = "Winkle Area Digspot"
WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block"
@@ -232,21 +232,21 @@ class LocationName:
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"
AirportWestsideDigspot1 = "Airport Westside Digspot 1"
AirportWestsideDigspot2 = "Airport Westside Digspot 2"
AirportWestsideDigspot3 = "Airport Westside Digspot 3"
AirportWestsideDigspot4 = "Airport Westside Digspot 4"
AirportWestsideDigspot5 = "Airport Westside 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"
AirportEastsideDigspot1 = "Airport Eastside Digspot 1"
AirportEastsideDigspot2 = "Airport Eastside Digspot 2"
AirportEastsideDigspot3 = "Airport Eastside Digspot 3"
AirportEastsideDigspot4 = "Airport Eastside Digspot 4"
AirportEastsideDigspot5 = "Airport Eastside Digspot 5"
GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot"
GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot"
GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot"
@@ -276,10 +276,10 @@ class LocationName:
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"
TeeheeValleyBeforeBirdoDigspot1 = "Teehee Valley Before Birdo Digspot 1"
TeeheeValleyBeforeBirdoDigspot2 = "Teehee Valley Before Birdo Digspot 2"
TeeheeValleyBeforeBirdoDigspot3 = "Teehee Valley Before Birdo Digspot 3"
TeeheeValleyBeforeBirdoDigspot4 = "Teehee Valley Before Birdo Digspot 4"
TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1"
TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2"
TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3"
@@ -296,9 +296,9 @@ class LocationName:
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"
TeeheeValleyUpperMazeRoom2Digspot1 = "Teehee Valley Upper Maze Room 2 Digspot 1"
TeeheeValleyUpperMazeRoom2Digspot2 = "Teehee Valley Upper Maze Room 2 Digspot 2"
TeeheeValleyUpperMazeRoom1Block = "Teehee Valley Upper Maze Room 1 Block"
TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot"
TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot"
SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1"
@@ -314,10 +314,10 @@ class LocationName:
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"
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"
JokesEndNortheastOfBoilerRoom2Digspot = "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"
@@ -505,7 +505,7 @@ class LocationName:
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"
BowsersCastlePastMortonBlock = "Bowser's Castle Past 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"
@@ -546,7 +546,7 @@ class LocationName:
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"
ChucklehuckWoodsPastChucklerootCoinBlock = "Chucklehuck Woods Past Chuckleroot Coin Block"
ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block"
ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block"
SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block"
+3 -2
View File
@@ -1,4 +1,4 @@
from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range
from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range, Removed
from dataclasses import dataclass
@@ -282,7 +282,8 @@ class MLSSOptions(PerGameCommonOptions):
extra_pipes: ExtraPipes
skip_minecart: SkipMinecart
disable_surf: DisableSurf
harhalls_pants: HarhallsPants
disable_harhalls_pants: HarhallsPants
harhalls_pants: Removed
block_visibility: HiddenVisible
chuckle_beans: ChuckleBeans
music_options: MusicOptions
+38 -33
View File
@@ -33,6 +33,7 @@ from .Locations import (
postJokes,
baseUltraRocks,
coins,
cacklettas_soul,
)
from . import StateLogic
@@ -40,44 +41,45 @@ if typing.TYPE_CHECKING:
from . import MLSSWorld
def create_regions(world: "MLSSWorld", excluded: typing.List[str]):
def create_regions(world: "MLSSWorld"):
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)
create_region(world, "Main Area", mainArea)
create_region(world, "Chucklehuck Woods", chucklehuck)
create_region(world, "Beanbean Castle Town", castleTown)
create_region(world, "Shop Starting Flag", startingFlag)
create_region(world, "Shop Chuckolator Flag", chuckolatorFlag)
create_region(world, "Shop Mom Piranha Flag", piranhaFlag)
create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag)
create_region(world, "Shop Beanstar Complete Flag", beanstarFlag)
create_region(world, "Shop Birdo Flag", birdoFlag)
create_region(world, "Surfable", surfable)
create_region(world, "Hooniversity", hooniversity)
create_region(world, "GwarharEntrance", gwarharEntrance)
create_region(world, "GwarharMain", gwarharMain)
create_region(world, "TeeheeValley", teeheeValley)
create_region(world, "Winkle", winkle)
create_region(world, "Sewers", sewers)
create_region(world, "Airport", airport)
create_region(world, "JokesEntrance", jokesEntrance)
create_region(world, "JokesMain", jokesMain)
create_region(world, "PostJokes", postJokes)
create_region(world, "Theater", theater)
create_region(world, "Fungitown", fungitown)
create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar)
create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo)
create_region(world, "BooStatue", booStatue)
create_region(world, "Oasis", oasis)
create_region(world, "BaseUltraRocks", baseUltraRocks)
create_region(world, "Cackletta's Soul", cacklettas_soul)
if world.options.coins:
create_region(world, "Coins", coins, excluded)
create_region(world, "Coins", coins)
if not world.options.castle_skip:
create_region(world, "Bowser's Castle", bowsers, excluded)
create_region(world, "Bowser's Castle Mini", bowsersMini, excluded)
create_region(world, "Bowser's Castle", bowsers)
create_region(world, "Bowser's Castle Mini", bowsersMini)
def connect_regions(world: "MLSSWorld"):
@@ -221,6 +223,9 @@ def connect_regions(world: "MLSSWorld"):
"Bowser's Castle Mini",
lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player),
)
connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul")
else:
connect(world, names, "PostJokes", "Cackletta's Soul")
connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player))
connect(
world,
@@ -282,11 +287,11 @@ def connect_regions(world: "MLSSWorld"):
)
def create_region(world: "MLSSWorld", name, locations, excluded):
def create_region(world: "MLSSWorld", name, locations):
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:
if location.name in world.disabled_locations:
continue
ret.locations.append(loc)
world.multiworld.regions.append(ret)
+15 -18
View File
@@ -8,7 +8,7 @@ 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
from .Locations import shop, badge, pants, location_table, all_locations
if TYPE_CHECKING:
from . import MLSSWorld
@@ -88,7 +88,7 @@ class MLSSPatchExtension(APPatchExtension):
return rom
stream = io.BytesIO(rom)
for location in all_locations:
for location in [location for location in all_locations if location.itemType == 0]:
stream.seek(location.id - 6)
b = stream.read(1)
if b[0] == 0x10 and options["block_visibility"] == 1:
@@ -133,7 +133,7 @@ class MLSSPatchExtension(APPatchExtension):
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:
if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2 and options["randomize_enemies"] == 0):
raw = []
for pos in bosses:
stream.seek(pos + 1)
@@ -164,6 +164,7 @@ class MLSSPatchExtension(APPatchExtension):
enemies_raw = []
groups = []
boss_groups = []
if options["randomize_enemies"] == 0:
return stream.getvalue()
@@ -171,7 +172,7 @@ class MLSSPatchExtension(APPatchExtension):
if options["randomize_bosses"] == 2:
for pos in bosses:
stream.seek(pos + 1)
groups += [stream.read(0x1F)]
boss_groups += [stream.read(0x1F)]
for pos in enemies:
stream.seek(pos + 8)
@@ -221,12 +222,19 @@ class MLSSPatchExtension(APPatchExtension):
groups += [raw]
chomp = False
random.shuffle(groups)
arr = enemies
if options["randomize_bosses"] == 2:
arr += bosses
groups += boss_groups
random.shuffle(groups)
for pos in arr:
if arr[-1] in boss_groups:
stream.seek(pos)
temp = stream.read(1)
stream.seek(pos)
stream.write(bytes([temp[0] | 0x8]))
stream.seek(pos + 1)
stream.write(groups.pop())
@@ -320,20 +328,9 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
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)
):
if location_name in world.disabled_locations:
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)
location = world.get_location(location_name)
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)
+157 -24
View File
@@ -13,7 +13,7 @@ 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
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:
@@ -218,9 +218,9 @@ def set_rules(world: "MLSSWorld", excluded):
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),
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),
@@ -235,27 +235,27 @@ def set_rules(world: "MLSSWorld", excluded):
lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock1),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock2),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock3),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock4),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock5),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock6),
lambda state: StateLogic.fruits(state, world.player),
)
add_rule(
@@ -350,10 +350,6 @@ def set_rules(world: "MLSSWorld", excluded):
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),
@@ -462,6 +458,143 @@ def set_rules(world: "MLSSWorld", excluded):
lambda state: StateLogic.canCrash(state, world.player),
)
if world.options.randomize_bosses.value != 0:
if world.options.chuckle_beans != 0:
add_rule(
world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot1),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPastHoohoorosDigspot),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot1),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBelowSummitDigspot),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainSummitDigspot),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
if world.options.chuckle_beans == 2:
add_rule(
world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot2),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot2),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooVillageHammers),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPeasleysRose),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock1),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock2),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBelowSummitBlock1),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBelowSummitBlock2),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainBelowSummitBlock3),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPastHoohoorosBlock1),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPastHoohoorosBlock2),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
add_rule(
world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomBlock),
lambda state: StateLogic.hammers(state, world.player)
or StateLogic.fire(state, world.player)
or StateLogic.thunder(state, world.player),
)
if not world.options.difficult_logic:
if world.options.chuckle_beans != 0:
add_rule(
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Digspot),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom3Digspot),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom1Block),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Block1),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndFurnaceRoom1Block1),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndFurnaceRoom1Block2),
lambda state: StateLogic.canCrash(state, world.player),
)
add_rule(
world.get_location(LocationName.JokesEndFurnaceRoom1Block3),
lambda state: StateLogic.canCrash(state, world.player),
)
if world.options.coins:
add_rule(
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1),
@@ -516,7 +649,7 @@ def set_rules(world: "MLSSWorld", excluded):
lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
)
add_rule(
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock),
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootCoinBlock),
lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player),
)
add_rule(
@@ -546,23 +679,23 @@ def set_rules(world: "MLSSWorld", excluded):
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)),
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))
),
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)),
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(
+23 -42
View File
@@ -4,7 +4,7 @@ import typing
import settings
from BaseClasses import Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World
from typing import List, Dict, Any
from typing import Set, 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
@@ -55,29 +55,29 @@ class MLSSWorld(World):
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)
required_client_version = (0, 5, 0)
disabled_locations: List[str]
disabled_locations: Set[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]
self.disabled_locations = set()
if self.options.skip_minecart:
self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot]
self.disabled_locations.update([LocationName.HoohooMountainBaseMinecartCaveDigspot])
if self.options.disable_surf:
self.disabled_locations += [LocationName.SurfMinigame]
if self.options.harhalls_pants:
self.disabled_locations += [LocationName.HarhallsPants]
self.disabled_locations.update([LocationName.SurfMinigame])
if self.options.disable_harhalls_pants:
self.disabled_locations.update([LocationName.HarhallsPants])
if self.options.chuckle_beans == 0:
self.disabled_locations.update([location.name for location in all_locations if "Digspot" in location.name])
if self.options.chuckle_beans == 1:
self.disabled_locations.update([location.name for location in all_locations if location.id in hidden])
if self.options.castle_skip:
self.disabled_locations.update([location.name for location in bowsers + bowsersMini])
if not self.options.coins:
self.disabled_locations += [location.name for location in all_locations if location in coins]
self.disabled_locations.update([location.name for location in coins])
def create_regions(self) -> None:
create_regions(self, self.disabled_locations)
create_regions(self)
connect_regions(self)
item = self.create_item("Mushroom")
@@ -90,13 +90,15 @@ class MLSSWorld(World):
self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item)
item = self.create_item("Chuckle Bean")
self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item)
item = MLSSItem("Victory", ItemClassification.progression, None, self.player)
self.get_location("Cackletta's Soul").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,
"HarhallsPants": self.options.disable_harhalls_pants.value,
"ChuckleBeans": self.options.chuckle_beans.value,
"DifficultLogic": self.options.difficult_logic.value,
"Coins": self.options.coins.value,
@@ -111,7 +113,7 @@ class MLSSWorld(World):
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:
if self.options.disable_harhalls_pants and "Harhall's" in item.itemName:
continue
required_items += [item.itemName for _ in range(freq)]
@@ -135,21 +137,7 @@ class MLSSWorld(World):
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)
remaining = len(all_locations) - len(required_items) - len(self.disabled_locations) - 5
self.multiworld.itempool += [
self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining)
@@ -157,21 +145,14 @@ class MLSSWorld(World):
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
)
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", 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)))
return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))).itemName
def generate_output(self, output_directory: str) -> None:
patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
Binary file not shown.
+1 -1
View File
@@ -37,7 +37,7 @@ weapons_to_name: Dict[int, str] = {
minimum_weakness_requirement: Dict[int, int] = {
0: 1, # Mega Buster is free
1: 14, # 2 shots of Atomic Fire
2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot
2: 2, # 14 shots of Air Shooter
3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off
4: 1, # 56 uses of Bubble Lead
5: 1, # 224 uses of Quick Boomerang
+27 -1
View File
@@ -97,6 +97,28 @@ class MMBN3World(World):
add_item_rule(loc, lambda item: not item.advancement)
region.locations.append(loc)
self.multiworld.regions.append(region)
# Regions which contribute to explore score when accessible.
explore_score_region_names = (
RegionName.WWW_Island,
RegionName.SciLab_Overworld,
RegionName.SciLab_Cyberworld,
RegionName.Yoka_Overworld,
RegionName.Yoka_Cyberworld,
RegionName.Beach_Overworld,
RegionName.Beach_Cyberworld,
RegionName.Undernet,
RegionName.Deep_Undernet,
RegionName.Secret_Area,
)
explore_score_regions = [self.get_region(region_name) for region_name in explore_score_region_names]
# Entrances which use explore score in their logic need to register all the explore score regions as indirect
# conditions.
def register_explore_score_indirect_conditions(entrance):
for explore_score_region in explore_score_regions:
self.multiworld.register_indirect_condition(explore_score_region, entrance)
for region_info in regions:
region = name_to_region[region_info.name]
for connection in region_info.connections:
@@ -119,6 +141,7 @@ class MMBN3World(World):
entrance.access_rule = lambda state: \
state.has(ItemName.CSciPas, self.player) or \
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player)
self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance)
if connection == RegionName.Yoka_Cyberworld:
entrance.access_rule = lambda state: \
state.has(ItemName.CYokaPas, self.player) or \
@@ -126,16 +149,19 @@ class MMBN3World(World):
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and
state.has(ItemName.Press, self.player)
)
self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance)
if connection == RegionName.Beach_Cyberworld:
entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.register_indirect_condition(self.get_region(RegionName.Yoka_Overworld), entrance)
if connection == RegionName.Undernet:
entrance.access_rule = lambda state: self.explore_score(state) > 8 and\
state.has(ItemName.Press, self.player)
register_explore_score_indirect_conditions(entrance)
if connection == RegionName.Secret_Area:
entrance.access_rule = lambda state: self.explore_score(state) > 12 and\
state.has(ItemName.Hammer, self.player)
register_explore_score_indirect_conditions(entrance)
if connection == RegionName.WWW_Island:
entrance.access_rule = lambda state:\
state.has(ItemName.Progressive_Undernet_Rank, self.player, 8)
+20 -21
View File
@@ -1,9 +1,9 @@
from .Utils import data_path, __version__
from .Colors import *
import logging
import worlds.oot.Music as music
import worlds.oot.Sounds as sfx
import worlds.oot.IconManip as icon
from . import Music as music
from . import Sounds as sfx
from . import IconManip as icon
from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
import json
@@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols):
# handle random
if tunic_option == 'Random Choice':
tunic_option = random.choice(tunic_color_list)
tunic_option = ootworld.random.choice(tunic_color_list)
# handle completely random
if tunic_option == 'Completely Random':
color = generate_random_color()
@@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols):
# choose a random choice for the whole group
if navi_option_inner == 'Random Choice':
navi_option_inner = random.choice(navi_color_list)
navi_option_inner = ootworld.random.choice(navi_color_list)
if navi_option_outer == 'Random Choice':
navi_option_outer = random.choice(navi_color_list)
navi_option_outer = ootworld.random.choice(navi_color_list)
if navi_option_outer == 'Match Inner':
navi_option_outer = navi_option_inner
@@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols):
# handle random choice
if option_inner == 'Random Choice':
option_inner = random.choice(sword_trail_color_list)
option_inner = ootworld.random.choice(sword_trail_color_list)
if option_outer == 'Random Choice':
option_outer = random.choice(sword_trail_color_list)
option_outer = ootworld.random.choice(sword_trail_color_list)
if option_outer == 'Match Inner':
option_outer = option_inner
@@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails):
# handle random choice
if option_inner == 'Random Choice':
option_inner = random.choice(trail_color_list)
option_inner = ootworld.random.choice(trail_color_list)
if option_outer == 'Random Choice':
option_outer = random.choice(trail_color_list)
option_outer = ootworld.random.choice(trail_color_list)
if option_outer == 'Match Inner':
option_outer = option_inner
@@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols):
# handle random
if gauntlet_option == 'Random Choice':
gauntlet_option = random.choice(gauntlet_color_list)
gauntlet_option = ootworld.random.choice(gauntlet_color_list)
# handle completely random
if gauntlet_option == 'Completely Random':
color = generate_random_color()
@@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols):
# handle random
if shield_frame_option == 'Random Choice':
shield_frame_option = random.choice(shield_frame_color_list)
shield_frame_option = ootworld.random.choice(shield_frame_color_list)
# handle completely random
if shield_frame_option == 'Completely Random':
color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)]
# grab the color from the list
elif shield_frame_option in shield_frame_colors:
color = list(shield_frame_colors[shield_frame_option])
@@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols):
# handle random
if heart_option == 'Random Choice':
heart_option = random.choice(heart_color_list)
heart_option = ootworld.random.choice(heart_color_list)
# handle completely random
if heart_option == 'Completely Random':
color = generate_random_color()
@@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols):
magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
if magic_option == 'Random Choice':
magic_option = random.choice(magic_color_list)
magic_option = ootworld.random.choice(magic_color_list)
if magic_option == 'Completely Random':
color = generate_random_color()
@@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols):
# handle random
if button_option == 'Random Choice':
button_option = random.choice(list(button_colors.keys()))
button_option = ootworld.random.choice(list(button_colors.keys()))
# handle completely random
if button_option == 'Completely Random':
fixed_font_color = [10, 10, 10]
@@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols):
rom.write_int16(loc, sound_id)
else:
if selection == 'random-choice':
selection = random.choice(sfx.get_hook_pool(hook)).value.keyword
selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword
elif selection == 'random-ear-safe':
selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
elif selection == 'completely-random':
selection = random.choice(sfx.standard).value.keyword
selection = ootworld.random.choice(sfx.standard).value.keyword
sound_id = sound_dict[selection]
for loc in hook.value.locations:
rom.write_int16(loc, sound_id)
@@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols):
choice = ootworld.sfx_ocarina
if choice == 'random-choice':
choice = random.choice(list(instruments.keys()))
choice = ootworld.random.choice(list(instruments.keys()))
rom.write_byte(0x00B53C7B, instruments[choice])
rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
@@ -769,7 +769,6 @@ patch_sets[0x1F073FD9] = {
def patch_cosmetics(ootworld, rom):
# Use the world's slot seed for cosmetics
random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random())
# try to detect the cosmetic patch data format
versioned_patch_set = None
+2 -2
View File
@@ -3,9 +3,9 @@ from BaseClasses import Entrance
class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
def __init__(self, player, world, name='', parent=None):
def __init__(self, player, multiworld, name='', parent=None):
super(OOTEntrance, self).__init__(player, name, parent)
self.multiworld = world
self.multiworld = multiworld
self.access_rules = []
self.reverse = None
self.replaces = None
+20 -20
View File
@@ -440,16 +440,16 @@ class EntranceShuffleError(Exception):
def shuffle_random_entrances(ootworld):
world = ootworld.multiworld
multiworld = ootworld.multiworld
player = ootworld.player
# Gather locations to keep reachable for validation
all_state = ootworld.get_state_with_complete_itempool()
all_state.sweep_for_advancements(locations=ootworld.get_locations())
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
locations_to_ensure_reachable = {loc for loc in multiworld.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances
set_all_entrances_data(world, player)
set_all_entrances_data(multiworld, player)
# Determine entrance pools based on settings
one_way_entrance_pools = {}
@@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld):
none_state = CollectionState(ootworld.multiworld)
# Plando entrances
if world.plando_connections[player]:
if ootworld.options.plando_connections:
rollbacks = []
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
for conn in world.plando_connections[player]:
for conn in ootworld.options.plando_connections:
try:
entrance = ootworld.get_entrance(conn.entrance)
exit = ootworld.get_entrance(conn.exit)
@@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld):
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable
new_all_state = ootworld.get_state_with_complete_itempool()
if not world.has_beaten_game(new_all_state, player):
if not multiworld.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game')
# Validate world
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
@@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
ootworld.multiworld.random.shuffle(avail_pool)
ootworld.random.shuffle(avail_pool)
for entrance in avail_pool:
if entrance.replaces:
@@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances,
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
ootworld.multiworld.random.shuffle(entrances)
ootworld.random.shuffle(entrances)
for entrance in entrances:
if entrance.connected_region != None:
continue
ootworld.multiworld.random.shuffle(target_entrances)
ootworld.random.shuffle(target_entrances)
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
# success rate over randomization
if pool_type in {'InteriorSoft', 'MixedSoft'}:
@@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
# TODO: improve this function
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
world = ootworld.multiworld
multiworld = ootworld.multiworld
player = ootworld.player
all_state = all_state_orig.copy()
@@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
elif (potion_front and not potion_back) or (not potion_front and potion_back):
@@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
if ootworld.shuffle_cows:
impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
elif (impas_front and not impas_back) or (not impas_front and impas_back):
@@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
raise EntranceShuffleError('Path to ToT as child not guaranteed')
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
# Ensure big poe shop is always reachable as adult
if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
if ootworld.shopsanity == 'off':
# Ensure that Goron and Zora shops are accessible as adult
if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Goron City Shop not accessible as adult')
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
if ootworld.open_forest == 'closed':
# Ensure that Kokiri Shop is reachable as child with no items
if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')
+11 -13
View File
@@ -1,5 +1,3 @@
import random
from BaseClasses import LocationProgressType
from .Items import OOTItem
@@ -28,7 +26,7 @@ class Hint(object):
text = ""
type = []
def __init__(self, name, text, type, choice=None):
def __init__(self, name, text, type, rand, choice=None):
self.name = name
self.type = [type] if not isinstance(type, list) else type
@@ -36,31 +34,31 @@ class Hint(object):
self.text = text
else:
if choice == None:
self.text = random.choice(text)
self.text = rand.choice(text)
else:
self.text = text[choice]
def getHint(item, clearer_hint=False):
def getHint(item, rand, clearer_hint=False):
if item in hintTable:
textOptions, clearText, hintType = hintTable[item]
if clearer_hint:
if clearText == None:
return Hint(item, textOptions, hintType, 0)
return Hint(item, clearText, hintType)
return Hint(item, textOptions, hintType, rand, 0)
return Hint(item, clearText, hintType, rand)
else:
return Hint(item, textOptions, hintType)
return Hint(item, textOptions, hintType, rand)
elif isinstance(item, str):
return Hint(item, item, 'generic')
return Hint(item, item, 'generic', rand)
else: # is an Item
return Hint(item.name, item.hint_text, 'item')
return Hint(item.name, item.hint_text, 'item', rand)
def getHintGroup(group, world):
ret = []
for name in hintTable:
hint = getHint(name, world.clearer_hints)
hint = getHint(name, world.random, world.clearer_hints)
if hint.name in world.always_hints and group == 'always':
hint.type = 'always'
@@ -95,7 +93,7 @@ def getHintGroup(group, world):
def getRequiredHints(world):
ret = []
for name in hintTable:
hint = getHint(name)
hint = getHint(name, world.random)
if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world):
ret.append(hint)
return ret
@@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False):
location_hints = []
for name in hintTable:
hint = getHint(name, world.clearer_hints)
hint = getHint(name, world.random, world.clearer_hints)
if any(item in hint.type for item in
['always',
'dual_always',
+33 -33
View File
@@ -136,13 +136,13 @@ def getItemGenericName(item):
def isRestrictedDungeonItem(dungeon, item):
if not isinstance(item, OOTItem):
return False
if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon':
if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon':
return item in dungeon.dungeon_items
if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon':
if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon':
return item in dungeon.small_keys
if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon':
if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon':
return item in dungeon.boss_key
if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon':
if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon':
return item in dungeon.boss_key
return False
@@ -261,8 +261,8 @@ hintPrefixes = [
'',
]
def getSimpleHintNoPrefix(item):
hint = getHint(item.name, True).text
def getSimpleHintNoPrefix(item, rand):
hint = getHint(item.name, rand, True).text
for prefix in hintPrefixes:
if hint.startswith(prefix):
@@ -417,9 +417,9 @@ class HintArea(Enum):
# Formats the hint text for this area with proper grammar.
# Dungeons are hinted differently depending on the clearer_hints setting.
def text(self, clearer_hints, preposition=False, world=None):
def text(self, rand, clearer_hints, preposition=False, world=None):
if self.is_dungeon:
text = getHint(self.dungeon_name, clearer_hints).text
text = getHint(self.dungeon_name, rand, clearer_hints).text
else:
text = str(self)
prefix, suffix = text.replace('#', '').split(' ', 1)
@@ -489,7 +489,7 @@ def get_woth_hint(world, checked):
if getattr(location.parent_region, "dungeon", None):
world.woth_dungeon += 1
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text
else:
location_text = get_hint_area(location)
@@ -570,9 +570,9 @@ def get_good_item_hint(world, checked):
location = world.hint_rng.choice(locations)
checked[location.player].add(location.name)
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
if getattr(location.parent_region, "dungeon", None):
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
else:
@@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked):
location = world.hint_rng.choice(locations)
checked[location.player].add(location.name)
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
if getattr(location.parent_region, "dungeon", None):
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
if world.hint_dist_user.get('vague_named_items', False):
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
else:
@@ -648,9 +648,9 @@ def get_random_location_hint(world, checked):
checked[location.player].add(location.name)
dungeon = location.parent_region.dungeon
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
if dungeon:
location_text = getHint(dungeon.name, world.clearer_hints).text
location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
else:
@@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type):
location_text = hint.text
if '#' not in location_text:
location_text = '#%s#' % location_text
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
@@ -724,9 +724,9 @@ def get_entrance_hint(world, checked):
connected_region = entrance.connected_region
if connected_region.dungeon:
region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text
region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text
else:
region_text = getHint(connected_region.name, world.clearer_hints).text
region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text
if '#' not in region_text:
region_text = '#%s#' % region_text
@@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None):
if location.name in world.hint_text_overrides:
location_text = world.hint_text_overrides[location.name]
else:
location_text = getHint(location.name, world.clearer_hints).text
location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text
if '#' not in location_text:
location_text = '#%s#' % location_text
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
logging.getLogger('').debug('Placed always hint for %s.', location.name)
@@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
('Goron Ruby', 'Red'),
('Zora Sapphire', 'Blue'),
]
child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04'
child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
for (reward, color) in bossRewardsSpiritualStones:
child_text += buildBossString(reward, color, world)
child_text += getHint('Child Altar Text End', world.clearer_hints).text
child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text
child_text += '\x0B'
update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
# text that appears at altar as an adult.
adult_text = '\x08'
adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04'
adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
if include_rewards:
bossRewardsMedallions = [
('Light Medallion', 'Light Blue'),
@@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
adult_text += '\x04'
adult_text += buildGanonBossKeyString(world)
else:
adult_text += getHint('Adult Altar Text End', world.clearer_hints).text
adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text
adult_text += '\x0B'
update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
@@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world):
text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='')
else:
location = world.hinted_dungeon_reward_locations[reward]
location_text = HintArea.at(location).text(world.clearer_hints, preposition=True)
location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True)
text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='')
return str(text) + '\x04'
@@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world):
if world.bridge == 'open':
string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
else:
item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text
item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text
if world.bridge == 'medallions':
item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
elif world.bridge == 'stones':
@@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world):
string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
else:
if world.shuffle_ganon_bosskey == 'on_lacs':
item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text
item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text
if world.lacs_condition == 'medallions':
item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
elif world.lacs_condition == 'stones':
@@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world):
item_req_string = '#%s#' % item_req_string
bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']:
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
if world.shuffle_ganon_bosskey == 'medallions':
item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string
elif world.shuffle_ganon_bosskey == 'stones':
@@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world):
item_req_string = '#%s#' % item_req_string
bk_location_string = "automatically granted once %s are retrieved" % item_req_string
else:
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
return str(GossipText(string, ['Yellow'], prefix=''))
@@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages):
if location.player != world.player:
player_text = world.multiworld.get_player_name(location.player) + "'s "
if location.game == 'Ocarina of Time':
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None)
else:
area = location.name
text = data['default_item_text'].format(area=rom_safe_text(player_text + area))
elif 'default_item_fallback' in data:
text = data['default_item_fallback']
else:
text = getHint('Validation Line', world.clearer_hints).text
text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text
location = world.get_location('Ganons Tower Boss Key Chest')
text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#"
text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#"
for find, replace in data.get('replace', {}).items():
text = text.replace(find, replace)
@@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages):
if hint_type in world.misc_hints:
location = world.get_location(data['item_location'])
item = location.item
item_text = getHint(getItemGenericName(item), world.clearer_hints).text
item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text
if item.player != world.player:
item_text += f' for {world.multiworld.get_player_name(item.player)}'
text = data['location_text'].format(item=rom_safe_text(item_text))
+22 -24
View File
@@ -295,16 +295,14 @@ random = None
def get_junk_pool(ootworld):
junk_pool[:] = list(junk_pool_base)
if ootworld.junk_ice_traps == 'on':
if ootworld.options.junk_ice_traps == 'on':
junk_pool.append(('Ice Trap', 10))
elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']:
elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']:
junk_pool[:] = [('Ice Trap', 1)]
return junk_pool
def get_junk_item(count=1, pool=None, plando_pool=None):
global random
def get_junk_item(rand, count=1, pool=None, plando_pool=None):
if count < 1:
raise ValueError("get_junk_item argument 'count' must be greater than 0.")
@@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None):
raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
else:
junk_items, junk_weights = zip(*junk_pool)
return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count))
return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count))
return return_pool
def replace_max_item(items, item, max):
def replace_max_item(items, item, max, rand):
count = 0
for i,val in enumerate(items):
if val == item:
if count >= max:
items[i] = get_junk_item()[0]
items[i] = get_junk_item(rand)[0]
count += 1
@@ -375,7 +373,7 @@ def get_pool_core(world):
pending_junk_pool.append('Kokiri Sword')
if world.shuffle_ocarinas:
pending_junk_pool.append('Ocarina')
if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0):
if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0):
pending_junk_pool.append('Magic Bean Pack')
if (world.gerudo_fortress != "open"
and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']):
@@ -450,7 +448,7 @@ def get_pool_core(world):
else:
item = deku_scrubs_items[location.vanilla_item]
if isinstance(item, list):
item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
shuffle_item = True
# Kokiri Sword
@@ -489,7 +487,7 @@ def get_pool_core(world):
# Cows
elif location.vanilla_item == 'Milk':
if world.shuffle_cows:
item = get_junk_item()[0]
item = get_junk_item(world.random)[0]
shuffle_item = world.shuffle_cows
if not shuffle_item:
location.show_in_spoiler = False
@@ -508,13 +506,13 @@ def get_pool_core(world):
item = 'Rutos Letter'
ruto_bottles -= 1
else:
item = random.choice(normal_bottles)
item = world.random.choice(normal_bottles)
shuffle_item = True
# Magic Beans
elif location.vanilla_item == 'Buy Magic Bean':
if world.shuffle_beans:
item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0]
item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0]
shuffle_item = world.shuffle_beans
if not shuffle_item:
location.show_in_spoiler = False
@@ -528,7 +526,7 @@ def get_pool_core(world):
# Adult Trade Item
elif location.vanilla_item == 'Pocket Egg':
potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items
item = random.choice(sorted(potential_trade_items))
item = world.random.choice(sorted(potential_trade_items))
world.selected_adult_trade_item = item
shuffle_item = True
@@ -541,7 +539,7 @@ def get_pool_core(world):
shuffle_item = False
location.show_in_spoiler = False
if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings:
item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
# Freestanding Rupees and Hearts
elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']:
@@ -618,7 +616,7 @@ def get_pool_core(world):
elif dungeon.name in world.key_rings and not dungeon.small_keys:
item = dungeon.item_name("Small Key Ring")
elif dungeon.name in world.key_rings:
item = get_junk_item()[0]
item = get_junk_item(world.random)[0]
shuffle_item = True
# Any other item in a dungeon.
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
@@ -630,7 +628,7 @@ def get_pool_core(world):
if shuffle_setting in ['remove', 'startwith']:
world.multiworld.push_precollected(dungeon_collection[-1])
world.remove_from_start_inventory.append(dungeon_collection[-1].name)
item = get_junk_item()[0]
item = get_junk_item(world.random)[0]
shuffle_item = True
elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']:
dungeon_collection[-1].priority = True
@@ -658,9 +656,9 @@ def get_pool_core(world):
shop_non_item_count = len(world.shop_prices)
shop_item_count = shop_slots_count - shop_non_item_count
pool.extend(random.sample(remain_shop_items, shop_item_count))
pool.extend(world.random.sample(remain_shop_items, shop_item_count))
if shop_non_item_count:
pool.extend(get_junk_item(shop_non_item_count))
pool.extend(get_junk_item(world.random, shop_non_item_count))
# Extra rupees for shopsanity.
if world.shopsanity not in ['off', '0']:
@@ -706,19 +704,19 @@ def get_pool_core(world):
if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']:
placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)'
pool.extend(get_junk_item())
pool.extend(get_junk_item(world.random))
else:
placed_items['Gift from Sages'] = IGNORE_LOCATION
world.get_location('Gift from Sages').show_in_spoiler = False
if world.junk_ice_traps == 'off':
replace_max_item(pool, 'Ice Trap', 0)
replace_max_item(pool, 'Ice Trap', 0, world.random)
elif world.junk_ice_traps == 'onslaught':
for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
replace_max_item(pool, item, 0)
replace_max_item(pool, item, 0, world.random)
for item, maximum in item_difficulty_max[world.item_pool_value].items():
replace_max_item(pool, item, maximum)
replace_max_item(pool, item, maximum, world.random)
# world.distribution.alter_pool(world, pool)
@@ -748,7 +746,7 @@ def get_pool_core(world):
pending_item = pending_junk_pool.pop()
if not junk_candidates:
raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1))
junk_item = random.choice(junk_candidates)
junk_item = world.random.choice(junk_candidates)
junk_candidates.remove(junk_item)
pool.remove(junk_item)
pool.append(pending_item)
+2 -3
View File
@@ -1,6 +1,5 @@
# text details: https://wiki.cloudmodding.com/oot/Text_Format
import random
from .HintList import misc_item_hint_table, misc_location_hint_table
from .TextBox import line_wrap
from .Utils import find_last
@@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe
rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# shuffles the messages in the game, making sure to keep various message types in their own group
def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True):
permutation = [i for i, _ in enumerate(messages)]
@@ -1002,7 +1001,7 @@ def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
def shuffle_group(group):
group_permutation = [i for i, _ in enumerate(group)]
random.shuffle(group_permutation)
rand.shuffle(group_permutation)
for index_from, index_to in enumerate(group_permutation):
permutation[group[index_to].index] = group[index_from].index
+8 -9
View File
@@ -1,6 +1,5 @@
#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
import random
import os
from .Utils import compare_version, data_path
@@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence
return sequences, target_sequences
def shuffle_music(sequences, target_sequences, music_mapping, log):
def shuffle_music(sequences, target_sequences, music_mapping, log, rand):
sequence_dict = {}
sequence_ids = []
@@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log):
# Shuffle the sequences
if len(sequences) < len(target_sequences):
raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
random.shuffle(sequence_ids)
rand.shuffle(sequence_ids)
sequences = []
for target_sequence in target_sequences:
@@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences):
rom.write_byte(base, j.instrument_set)
def shuffle_pointers_table(rom, ids, music_mapping, log):
def shuffle_pointers_table(rom, ids, music_mapping, log, rand):
# Read in all the Music data
bgm_data = {}
bgm_ids = []
@@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log):
bgm_ids.append(bgm[0])
# shuffle data
random.shuffle(bgm_ids)
rand.shuffle(bgm_ids)
# Write Music data back in random ordering
for bgm in ids:
@@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping):
# process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
# if ootworld.background_music == 'random_custom_only':
# sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random)
# if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
# process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
# if ootworld.fanfares == 'random_custom_only':
# fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random)
# if disabled_source_sequences:
# log = disable_music(rom, disabled_source_sequences.values(), log)
@@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping):
# rebuild_sequences(rom, sequences + fanfare_sequences)
# else:
if ootworld.background_music == 'randomized' or bgm_mapped:
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random)
if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random)
# end_else
if disabled_target_sequences:
log = disable_music(rom, disabled_target_sequences.values(), log)
+2 -3
View File
@@ -1,5 +1,4 @@
import struct
import random
import io
import array
import zlib
@@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue):
# xor_range is the range the XOR key will read from. This range is not
# too important, but I tried to choose from a section that didn't really
# have big gaps of 0s which we want to avoid.
def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)):
dma_start, dma_end = rom.get_dma_table_range()
# add header
@@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
# get random xor key. This range is chosen because it generally
# doesn't have many sections of 0s
xor_address = random.Random().randint(*xor_range)
xor_address = rand.randint(*xor_range)
patch_data.append_int32(xor_address)
new_buffer = copy.copy(rom.original.buffer)
+166 -19
View File
@@ -1,6 +1,8 @@
import typing
import random
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections
from dataclasses import dataclass
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \
PerGameCommonOptions, OptionGroup
from .EntranceShuffle import entrance_shuffle_table
from .LogicTricks import normalized_name_tricks
from .ColorSFXOptions import *
@@ -1281,21 +1283,166 @@ class LogicTricks(OptionList):
valid_keys_casefold = True
# All options assembled into a single dict
oot_options: typing.Dict[str, type(Option)] = {
"plando_connections": OoTPlandoConnections,
"logic_rules": Logic,
"logic_no_night_tokens_without_suns_song": NightTokens,
**open_options,
**world_options,
**bridge_options,
**dungeon_items_options,
**shuffle_options,
**timesavers_options,
**misc_options,
**itempool_options,
**cosmetic_options,
**sfx_options,
"logic_tricks": LogicTricks,
"death_link": DeathLink,
}
@dataclass
class OoTOptions(PerGameCommonOptions):
plando_connections: OoTPlandoConnections
death_link: DeathLink
logic_rules: Logic
logic_no_night_tokens_without_suns_song: NightTokens
logic_tricks: LogicTricks
open_forest: Forest
open_kakariko: Gate
open_door_of_time: DoorOfTime
zora_fountain: Fountain
gerudo_fortress: Fortress
bridge: Bridge
trials: Trials
starting_age: StartingAge
shuffle_interior_entrances: InteriorEntrances
shuffle_grotto_entrances: GrottoEntrances
shuffle_dungeon_entrances: DungeonEntrances
shuffle_overworld_entrances: OverworldEntrances
owl_drops: OwlDrops
warp_songs: WarpSongs
spawn_positions: SpawnPositions
shuffle_bosses: BossEntrances
# mix_entrance_pools: MixEntrancePools
# decouple_entrances: DecoupleEntrances
triforce_hunt: TriforceHunt
triforce_goal: TriforceGoal
extra_triforce_percentage: ExtraTriforces
bombchus_in_logic: LogicalChus
dungeon_shortcuts: DungeonShortcuts
dungeon_shortcuts_list: DungeonShortcutsList
mq_dungeons_mode: MQDungeons
mq_dungeons_list: MQDungeonList
mq_dungeons_count: MQDungeonCount
# empty_dungeons_mode: EmptyDungeons
# empty_dungeons_list: EmptyDungeonList
# empty_dungeon_count: EmptyDungeonCount
bridge_stones: BridgeStones
bridge_medallions: BridgeMedallions
bridge_rewards: BridgeRewards
bridge_tokens: BridgeTokens
bridge_hearts: BridgeHearts
shuffle_mapcompass: ShuffleMapCompass
shuffle_smallkeys: ShuffleKeys
shuffle_hideoutkeys: ShuffleGerudoKeys
shuffle_bosskeys: ShuffleBossKeys
enhance_map_compass: EnhanceMC
shuffle_ganon_bosskey: ShuffleGanonBK
ganon_bosskey_medallions: GanonBKMedallions
ganon_bosskey_stones: GanonBKStones
ganon_bosskey_rewards: GanonBKRewards
ganon_bosskey_tokens: GanonBKTokens
ganon_bosskey_hearts: GanonBKHearts
key_rings: KeyRings
key_rings_list: KeyRingList
shuffle_song_items: SongShuffle
shopsanity: ShopShuffle
shop_slots: ShopSlots
shopsanity_prices: ShopPrices
tokensanity: TokenShuffle
shuffle_scrubs: ScrubShuffle
shuffle_child_trade: ShuffleChildTrade
shuffle_freestanding_items: ShuffleFreestanding
shuffle_pots: ShufflePots
shuffle_crates: ShuffleCrates
shuffle_cows: ShuffleCows
shuffle_beehives: ShuffleBeehives
shuffle_kokiri_sword: ShuffleSword
shuffle_ocarinas: ShuffleOcarinas
shuffle_gerudo_card: ShuffleCard
shuffle_beans: ShuffleBeans
shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet
shuffle_frog_song_rupees: ShuffleFrogRupees
no_escape_sequence: SkipEscape
no_guard_stealth: SkipStealth
no_epona_race: SkipEponaRace
skip_some_minigame_phases: SkipMinigamePhases
complete_mask_quest: CompleteMaskQuest
useful_cutscenes: UsefulCutscenes
fast_chests: FastChests
free_scarecrow: FreeScarecrow
fast_bunny_hood: FastBunny
plant_beans: PlantBeans
chicken_count: ChickenCount
big_poe_count: BigPoeCount
fae_torch_count: FAETorchCount
correct_chest_appearances: CorrectChestAppearance
minor_items_as_major_chest: MinorInMajor
invisible_chests: InvisibleChests
correct_potcrate_appearances: CorrectPotCrateAppearance
hints: Hints
misc_hints: MiscHints
hint_dist: HintDistribution
text_shuffle: TextShuffle
damage_multiplier: DamageMultiplier
deadly_bonks: DeadlyBonks
no_collectible_hearts: HeroMode
starting_tod: StartingToD
blue_fire_arrows: BlueFireArrows
fix_broken_drops: FixBrokenDrops
start_with_consumables: ConsumableStart
start_with_rupees: RupeeStart
item_pool_value: ItemPoolValue
junk_ice_traps: IceTraps
ice_trap_appearance: IceTrapVisual
adult_trade_start: AdultTradeStart
default_targeting: Targeting
display_dpad: DisplayDpad
dpad_dungeon_menu: DpadDungeonMenu
correct_model_colors: CorrectColors
background_music: BackgroundMusic
fanfares: Fanfares
ocarina_fanfares: OcarinaFanfares
kokiri_color: kokiri_color
goron_color: goron_color
zora_color: zora_color
silver_gauntlets_color: silver_gauntlets_color
golden_gauntlets_color: golden_gauntlets_color
mirror_shield_frame_color: mirror_shield_frame_color
navi_color_default_inner: navi_color_default_inner
navi_color_default_outer: navi_color_default_outer
navi_color_enemy_inner: navi_color_enemy_inner
navi_color_enemy_outer: navi_color_enemy_outer
navi_color_npc_inner: navi_color_npc_inner
navi_color_npc_outer: navi_color_npc_outer
navi_color_prop_inner: navi_color_prop_inner
navi_color_prop_outer: navi_color_prop_outer
sword_trail_duration: SwordTrailDuration
sword_trail_color_inner: sword_trail_color_inner
sword_trail_color_outer: sword_trail_color_outer
bombchu_trail_color_inner: bombchu_trail_color_inner
bombchu_trail_color_outer: bombchu_trail_color_outer
boomerang_trail_color_inner: boomerang_trail_color_inner
boomerang_trail_color_outer: boomerang_trail_color_outer
heart_color: heart_color
magic_color: magic_color
a_button_color: a_button_color
b_button_color: b_button_color
c_button_color: c_button_color
start_button_color: start_button_color
sfx_navi_overworld: sfx_navi_overworld
sfx_navi_enemy: sfx_navi_enemy
sfx_low_hp: sfx_low_hp
sfx_menu_cursor: sfx_menu_cursor
sfx_menu_select: sfx_menu_select
sfx_nightfall: sfx_nightfall
sfx_horse_neigh: sfx_horse_neigh
sfx_hover_boots: sfx_hover_boots
sfx_ocarina: SfxOcarina
oot_option_groups: typing.List[OptionGroup] = [
OptionGroup("Open", [option for option in open_options.values()]),
OptionGroup("World", [*[option for option in world_options.values()],
*[option for option in bridge_options.values()]]),
OptionGroup("Shuffle", [option for option in shuffle_options.values()]),
OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]),
OptionGroup("Timesavers", [option for option in timesavers_options.values()]),
OptionGroup("Misc", [option for option in misc_options.values()]),
OptionGroup("Item Pool", [option for option in itempool_options.values()]),
OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]),
OptionGroup("SFX", [option for option in sfx_options.values()])
]
+13 -13
View File
@@ -208,8 +208,8 @@ def patch_rom(world, rom):
# Fix Ice Cavern Alcove Camera
if not world.dungeon_mq['Ice Cavern']:
rom.write_byte(0x2BECA25,0x01);
rom.write_byte(0x2BECA2D,0x01);
rom.write_byte(0x2BECA25,0x01)
rom.write_byte(0x2BECA2D,0x01)
# Fix GS rewards to be static
rom.write_int32(0xEA3934, 0)
@@ -944,7 +944,7 @@ def patch_rom(world, rom):
scene_table = 0x00B71440
for scene in range(0x00, 0x65):
scene_start = rom.read_int32(scene_table + (scene * 0x14));
scene_start = rom.read_int32(scene_table + (scene * 0x14))
add_scene_exits(scene_start)
return exit_table
@@ -1632,10 +1632,10 @@ def patch_rom(world, rom):
reward_text = None
elif getattr(location.item, 'looks_like_item', None) is not None:
jabu_item = location.item.looks_like_item
reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text)
reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text)
else:
jabu_item = location.item
reward_text = getHint(getItemGenericName(location.item), True).text
reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text
# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
if reward_text is None:
@@ -1687,7 +1687,7 @@ def patch_rom(world, rom):
# Sets hooks for gossip stone changes
symbol = rom.sym("GOSSIP_HINT_CONDITION");
symbol = rom.sym("GOSSIP_HINT_CONDITION")
if world.hints == 'none':
rom.write_int32(symbol, 0)
@@ -2264,9 +2264,9 @@ def patch_rom(world, rom):
# text shuffle
if world.text_shuffle == 'except_hints':
permutation = shuffle_messages(messages, except_hints=True)
permutation = shuffle_messages(messages, world.random, except_hints=True)
elif world.text_shuffle == 'complete':
permutation = shuffle_messages(messages, except_hints=False)
permutation = shuffle_messages(messages, world.random, except_hints=False)
# update warp song preview text boxes
update_warp_song_text(messages, world)
@@ -2358,7 +2358,7 @@ def patch_rom(world, rom):
# Write numeric seed truncated to 32 bits for rng seeding
# Overwritten with new seed every time a new rng value is generated
rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32)
rng_seed = world.random.getrandbits(32)
rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed)
# Static initial seed value for one-time random actions like the Hylian Shield discount
rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed)
@@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process
room_count = rom.read_byte(scene_data + 1)
room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
for _ in range(0, room_count):
room_data = rom.read_int32(room_list);
room_data = rom.read_int32(room_list)
if not room_data in processed_rooms:
actors.update(room_get_actors(rom, actor_func, room_data, scene))
@@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func):
actors = {}
scene_table = 0x00B71440
for scene in range(0x00, 0x65):
scene_data = rom.read_int32(scene_table + (scene * 0x14));
scene_data = rom.read_int32(scene_table + (scene * 0x14))
actors.update(scene_get_actors(rom, actor_func, scene_data, scene))
return actors
@@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags):
def remove_entrance_blockers(rom):
def remove_entrance_blockers_do(rom, actor_id, actor, scene):
if actor_id == 0x014E and scene == 97:
actor_var = rom.read_int16(actor + 14);
actor_var = rom.read_int16(actor + 14)
if actor_var == 0xFF01:
rom.write_int16(actor + 14, 0x0700)
get_actor_list(rom, remove_entrance_blockers_do)
@@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1])
else:
if item_display.game == "Ocarina of Time":
shop_item_name = getSimpleHintNoPrefix(item_display)
shop_item_name = getSimpleHintNoPrefix(item_display, world.random)
else:
shop_item_name = item_display.name
+1 -1
View File
@@ -64,7 +64,7 @@ class OOTRegion(Region):
return None
def can_reach(self, state):
if state.stale[self.player]:
if state._oot_stale[self.player]:
stored_age = state.age[self.player]
state._oot_update_age_reachable_regions(self.player)
state.age[self.player] = stored_age
+14 -14
View File
@@ -53,7 +53,7 @@ def isliteral(expr):
class Rule_AST_Transformer(ast.NodeTransformer):
def __init__(self, world, player):
self.multiworld = world
self.world = world
self.player = player
self.events = set()
# map Region -> rule ast string -> item name
@@ -86,9 +86,9 @@ class Rule_AST_Transformer(ast.NodeTransformer):
ctx=ast.Load()),
args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)],
keywords=[])
elif node.id in self.multiworld.__dict__:
elif node.id in self.world.__dict__:
# Settings are constant
return ast.parse('%r' % self.multiworld.__dict__[node.id], mode='eval').body
return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body
elif node.id in State.__dict__:
return self.make_call(node, node.id, [], [])
elif node.id in self.kwarg_defaults or node.id in allowed_globals:
@@ -137,7 +137,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
if isinstance(count, ast.Name):
# Must be a settings constant
count = ast.parse('%r' % self.multiworld.__dict__[count.id], mode='eval').body
count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body
if iname in escaped_items:
iname = escaped_items[iname]
@@ -182,7 +182,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
new_args = []
for child in node.args:
if isinstance(child, ast.Name):
if child.id in self.multiworld.__dict__:
if child.id in self.world.__dict__:
# child = ast.Attribute(
# value=ast.Attribute(
# value=ast.Name(id='state', ctx=ast.Load()),
@@ -190,7 +190,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
# ctx=ast.Load()),
# attr=child.id,
# ctx=ast.Load())
child = ast.Constant(getattr(self.multiworld, child.id))
child = ast.Constant(getattr(self.world, child.id))
elif child.id in rule_aliases:
child = self.visit(child)
elif child.id in escaped_items:
@@ -242,7 +242,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
# Fast check for json can_use
if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name)
and node.left.id not in self.multiworld.__dict__ and node.comparators[0].id not in self.multiworld.__dict__):
and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__):
return ast.NameConstant(node.left.id == node.comparators[0].id)
node.left = escape_or_string(node.left)
@@ -378,7 +378,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
# Requires the target regions have been defined in the world.
def create_delayed_rules(self):
for region_name, node, subrule_name in self.delayed_rules:
region = self.multiworld.multiworld.get_region(region_name, self.player)
region = self.world.multiworld.get_region(region_name, self.player)
event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True)
event.show_in_spoiler = False
@@ -395,7 +395,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
set_rule(event, access_rule)
region.locations.append(event)
self.multiworld.make_event_item(subrule_name, event)
self.world.make_event_item(subrule_name, event)
# Safeguard in case this is called multiple times per world
self.delayed_rules.clear()
@@ -448,7 +448,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
## Handlers for compile-time optimizations (former State functions)
def at_day(self, node):
if self.multiworld.ensure_tod_access:
if self.world.ensure_tod_access:
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
# parsing is better than constructing this expression by hand
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
@@ -456,7 +456,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
return ast.NameConstant(True)
def at_dampe_time(self, node):
if self.multiworld.ensure_tod_access:
if self.world.ensure_tod_access:
# tod has DAMPE or (tod == NONE and (find a path from a provider))
# parsing is better than constructing this expression by hand
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
@@ -464,10 +464,10 @@ class Rule_AST_Transformer(ast.NodeTransformer):
return ast.NameConstant(True)
def at_night(self, node):
if self.current_spot.type == 'GS Token' and self.multiworld.logic_no_night_tokens_without_suns_song:
if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song:
# Using visit here to resolve 'can_play' rule
return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body)
if self.multiworld.ensure_tod_access:
if self.world.ensure_tod_access:
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
# parsing is better than constructing this expression by hand
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
@@ -501,7 +501,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body
def current_spot_starting_age_access(self, node):
return self.current_spot_child_access(node) if self.multiworld.starting_age == 'child' else self.current_spot_adult_access(node)
return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node)
def has_bottle(self, node):
return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body
+18 -13
View File
@@ -8,12 +8,17 @@ from .Hints import HintArea
from .Items import oot_is_item_of_type
from .LocationList import dungeon_song_locations
from BaseClasses import CollectionState
from BaseClasses import CollectionState, MultiWorld
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
from ..AutoWorld import LogicMixin
from worlds.AutoWorld import LogicMixin
class OOTLogic(LogicMixin):
def init_mixin(self, parent: MultiWorld):
# Separate stale state for OOTRegion.can_reach() to use because CollectionState.update_reachable_regions() sets
# `self.state[player] = False` for all players without updating OOT's age region accessibility.
self._oot_stale = {player: True for player, world in parent.worlds.items()
if parent.worlds[player].game == "Ocarina of Time"}
def _oot_has_stones(self, count, player):
return self.has_group("stones", player, count)
@@ -92,9 +97,9 @@ class OOTLogic(LogicMixin):
return False
# Store the age before calling this!
def _oot_update_age_reachable_regions(self, player):
self.stale[player] = False
for age in ['child', 'adult']:
def _oot_update_age_reachable_regions(self, player):
self._oot_stale[player] = False
for age in ['child', 'adult']:
self.age[player] = age
rrp = getattr(self, f'{age}_reachable_regions')[player]
bc = getattr(self, f'{age}_blocked_connections')[player]
@@ -127,17 +132,17 @@ class OOTLogic(LogicMixin):
def set_rules(ootworld):
logger = logging.getLogger('')
world = ootworld.multiworld
multiworld = ootworld.multiworld
player = ootworld.player
if ootworld.logic_rules != 'no_logic':
if ootworld.triforce_hunt:
world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
multiworld.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
else:
world.completion_condition[player] = lambda state: state.has('Triforce', player)
multiworld.completion_condition[player] = lambda state: state.has('Triforce', player)
# ganon can only carry triforce
world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
multiworld.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
# is_child = ootworld.parser.parse_rule('is_child')
guarantee_hint = ootworld.parser.parse_rule('guarantee_hint')
@@ -151,22 +156,22 @@ def set_rules(ootworld):
if (ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon'
and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off'):
# First room chest needs to be a small key. Make sure the boss key isn't placed here.
location = world.get_location('Forest Temple MQ First Room Chest', player)
location = multiworld.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle.
location = world.get_location('Sheik in Ice Cavern', player)
location = multiworld.get_location('Sheik in Ice Cavern', player)
add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))
if ootworld.shuffle_child_trade == 'skip_child_zelda':
# Song from Impa must be local
location = world.get_location('Song from Impa', player)
location = multiworld.get_location('Song from Impa', player)
add_item_rule(location, lambda item: item.player == player)
for name in ootworld.always_hints:
add_rule(world.get_location(name, player), guarantee_hint)
add_rule(multiworld.get_location(name, player), guarantee_hint)
# TODO: re-add hints once they are working
# if location.type == 'HintStone' and ootworld.hints == 'mask':
+1 -1
View File
@@ -1,4 +1,4 @@
import worlds.oot.Messages as Messages
from . import Messages
# Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the
# characters on a line reach this value.
+51 -44
View File
@@ -20,7 +20,7 @@ from .ItemPool import generate_itempool, get_junk_item, get_junk_pool
from .Regions import OOTRegion, TimeOfDay
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
from .RuleParser import Rule_AST_Transformer
from .Options import oot_options
from .Options import OoTOptions, oot_option_groups
from .Utils import data_path, read_json
from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations
from .DungeonList import dungeon_table, create_dungeons
@@ -30,12 +30,12 @@ from .Patches import OoTContainer, patch_rom
from .N64Patch import create_patch_file
from .Cosmetics import patch_cosmetics
from Utils import get_options
from settings import get_settings
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections
from Fill import fill_restrictive, fast_fill, FillError
from worlds.generic.Rules import exclusion_rules, add_item_rule
from ..AutoWorld import World, AutoLogicRegister, WebWorld
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
i_o_limiter = threading.Semaphore(2)
@@ -128,6 +128,7 @@ class OOTWeb(WebWorld):
)
tutorials = [setup, setup_es, setup_fr, setup_de]
option_groups = oot_option_groups
class OOTWorld(World):
@@ -137,7 +138,8 @@ class OOTWorld(World):
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
"""
game: str = "Ocarina of Time"
option_definitions: dict = oot_options
options_dataclass = OoTOptions
options: OoTOptions
settings: typing.ClassVar[OOTSettings]
topology_present: bool = True
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
@@ -195,15 +197,15 @@ class OOTWorld(World):
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom = Rom(file=get_options()['oot_options']['rom_file'])
rom = Rom(file=get_settings()['oot_options']['rom_file'])
# Option parsing, handling incompatible options, building useful-item table
def generate_early(self):
self.parser = Rule_AST_Transformer(self, self.player)
for (option_name, option) in oot_options.items():
result = getattr(self.multiworld, option_name)[self.player]
for option_name in self.options_dataclass.type_hints:
result = getattr(self.options, option_name)
if isinstance(result, Range):
option_value = int(result)
elif isinstance(result, Toggle):
@@ -223,8 +225,8 @@ class OOTWorld(World):
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter()
self.songs_as_items = False
self.file_hash = [self.multiworld.random.randint(0, 31) for i in range(5)]
self.connect_name = ''.join(self.multiworld.random.choices(printable, k=16))
self.file_hash = [self.random.randint(0, 31) for i in range(5)]
self.connect_name = ''.join(self.random.choices(printable, k=16))
self.collectible_flag_addresses = {}
# Incompatible option handling
@@ -283,7 +285,7 @@ class OOTWorld(World):
local_types.append('BossKey')
if self.shuffle_ganon_bosskey != 'keysanity':
local_types.append('GanonBossKey')
self.multiworld.local_items[self.player].value |= set(name for name, data in item_table.items() if data[0] in local_types)
self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types)
# If any songs are itemlinked, set songs_as_items
for group in self.multiworld.groups.values():
@@ -297,7 +299,7 @@ class OOTWorld(World):
# Determine skipped trials in GT
# This needs to be done before the logic rules in GT are parsed
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
chosen_trials = self.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list}
# Determine tricks in logic
@@ -311,8 +313,8 @@ class OOTWorld(World):
# No Logic forces all tricks on, prog balancing off and beatable-only
elif self.logic_rules == 'no_logic':
self.multiworld.progression_balancing[self.player].value = False
self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
self.options.progression_balancing.value = False
self.options.accessibility.value = Accessibility.option_minimal
for trick in normalized_name_tricks.values():
setattr(self, trick['name'], True)
@@ -333,8 +335,8 @@ class OOTWorld(World):
# Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
self.trials_random = self.multiworld.trials[self.player].randomized
self.mq_dungeons_random = self.multiworld.mq_dungeons_count[self.player].randomized
self.trials_random = self.options.trials.randomized
self.mq_dungeons_random = self.options.mq_dungeons_count.randomized
self.easier_fire_arrow_entry = self.fae_torch_count < 24
if self.misc_hints:
@@ -393,8 +395,8 @@ class OOTWorld(World):
elif self.key_rings == 'choose':
self.key_rings = self.key_rings_list
elif self.key_rings == 'random_dungeons':
self.key_rings = self.multiworld.random.sample(keyring_dungeons,
self.multiworld.random.randint(0, len(keyring_dungeons)))
self.key_rings = self.random.sample(keyring_dungeons,
self.random.randint(0, len(keyring_dungeons)))
# Determine which dungeons are MQ. Not compatible with glitched logic.
mq_dungeons = set()
@@ -405,7 +407,7 @@ class OOTWorld(World):
elif self.mq_dungeons_mode == 'specific':
mq_dungeons = self.mq_dungeons_specific
elif self.mq_dungeons_mode == 'count':
mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count)
mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count)
else:
self.mq_dungeons_mode = 'count'
self.mq_dungeons_count = 0
@@ -425,8 +427,8 @@ class OOTWorld(World):
elif self.dungeon_shortcuts_choice == 'all':
self.dungeon_shortcuts = set(shortcut_dungeons)
elif self.dungeon_shortcuts_choice == 'random':
self.dungeon_shortcuts = self.multiworld.random.sample(shortcut_dungeons,
self.multiworld.random.randint(0, len(shortcut_dungeons)))
self.dungeon_shortcuts = self.random.sample(shortcut_dungeons,
self.random.randint(0, len(shortcut_dungeons)))
# == 'choice', leave as previous
else:
self.dungeon_shortcuts = set()
@@ -576,7 +578,7 @@ class OOTWorld(World):
new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region)
new_exit.vanilla_connected_region = exit
new_exit.rule_string = rule
if self.multiworld.logic_rules != 'none':
if self.options.logic_rules != 'no_logic':
self.parser.parse_spot_rule(new_exit)
if new_exit.never:
logger.debug('Dropping unreachable exit: %s', new_exit.name)
@@ -607,7 +609,7 @@ class OOTWorld(World):
elif self.shuffle_scrubs == 'random':
# this is a random value between 0-99
# average value is ~33 rupees
price = int(self.multiworld.random.betavariate(1, 2) * 99)
price = int(self.random.betavariate(1, 2) * 99)
# Set price in the dictionary as well as the location.
self.scrub_prices[scrub_item] = price
@@ -624,7 +626,7 @@ class OOTWorld(World):
self.shop_prices = {}
for region in self.regions:
if self.shopsanity == 'random':
shop_item_count = self.multiworld.random.randint(0, 4)
shop_item_count = self.random.randint(0, 4)
else:
shop_item_count = int(self.shopsanity)
@@ -632,17 +634,17 @@ class OOTWorld(World):
if location.type == 'Shop':
if location.name[-1:] in shop_item_indexes[:shop_item_count]:
if self.shopsanity_prices == 'normal':
self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5
self.shop_prices[location.name] = int(self.random.betavariate(1.5, 2) * 60) * 5
elif self.shopsanity_prices == 'affordable':
self.shop_prices[location.name] = 10
elif self.shopsanity_prices == 'starting_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,100,5)
self.shop_prices[location.name] = self.random.randrange(0,100,5)
elif self.shopsanity_prices == 'adults_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5)
self.shop_prices[location.name] = self.random.randrange(0,201,5)
elif self.shopsanity_prices == 'giants_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5)
self.shop_prices[location.name] = self.random.randrange(0,501,5)
elif self.shopsanity_prices == 'tycoons_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
self.shop_prices[location.name] = self.random.randrange(0,1000,5)
# Fill boss prizes
@@ -667,8 +669,8 @@ class OOTWorld(World):
while bossCount:
bossCount -= 1
self.multiworld.random.shuffle(prizepool)
self.multiworld.random.shuffle(prize_locs)
self.random.shuffle(prizepool)
self.random.shuffle(prize_locs)
item = prizepool.pop()
loc = prize_locs.pop()
loc.place_locked_item(item)
@@ -778,7 +780,7 @@ class OOTWorld(World):
# Call the junk fill and get a replacement
if item in self.itempool:
self.itempool.remove(item)
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
self.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool)))
if self.start_with_consumables:
self.starting_items['Deku Sticks'] = 30
self.starting_items['Deku Nuts'] = 40
@@ -881,7 +883,7 @@ class OOTWorld(World):
# Prefill shops, songs, and dungeon items
items = self.get_pre_fill_items()
locations = list(self.multiworld.get_unfilled_locations(self.player))
self.multiworld.random.shuffle(locations)
self.random.shuffle(locations)
# Set up initial state
state = CollectionState(self.multiworld)
@@ -910,7 +912,7 @@ class OOTWorld(World):
if isinstance(locations, list):
for item in stage_items:
self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
self.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
single_player_placement=True, lock=True, allow_excluded=True)
else:
@@ -923,7 +925,7 @@ class OOTWorld(World):
if isinstance(locations, list):
for item in dungeon_items:
self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
self.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
single_player_placement=True, lock=True, allow_excluded=True)
@@ -964,7 +966,7 @@ class OOTWorld(World):
while tries:
try:
self.multiworld.random.shuffle(song_locations)
self.random.shuffle(song_locations)
fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
single_player_placement=True, lock=True, allow_excluded=True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
@@ -996,7 +998,7 @@ class OOTWorld(World):
'Buy Goron Tunic': 1,
'Buy Zora Tunic': 1,
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
self.multiworld.random.shuffle(shop_locations)
self.random.shuffle(shop_locations)
self.pre_fill_items = [] # all prefill should be done
fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
single_player_placement=True, lock=True, allow_excluded=True)
@@ -1028,7 +1030,7 @@ class OOTWorld(World):
ganon_junk_fill = min(1, ganon_junk_fill)
gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons))
locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None]
junk_fill_locations = self.multiworld.random.sample(locations, round(len(locations) * ganon_junk_fill))
junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill))
exclusion_rules(self.multiworld, self.player, junk_fill_locations)
# Locations which are not sendable must be converted to events
@@ -1074,13 +1076,13 @@ class OOTWorld(World):
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
self.trap_appearances = {}
for loc_id in trap_location_ids:
self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name)
self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name)
# Seed hint RNG, used for ganon text lines also
self.hint_rng = self.multiworld.per_slot_randoms[self.player]
self.hint_rng = self.random
outfile_name = self.multiworld.get_out_file_name_base(self.player)
rom = Rom(file=get_options()['oot_options']['rom_file'])
rom = Rom(file=get_settings()['oot_options']['rom_file'])
try:
if self.hints != 'none':
buildWorldGossipHints(self)
@@ -1092,7 +1094,7 @@ class OOTWorld(World):
finally:
self.collectible_flags_available.set()
rom.update_header()
patch_data = create_patch_file(rom)
patch_data = create_patch_file(rom, self.random)
rom.restore()
apz5 = OoTContainer(patch_data, outfile_name, output_directory,
@@ -1301,6 +1303,7 @@ class OOTWorld(World):
# the appropriate number of keys in the collection state when they are
# picked up.
def collect(self, state: CollectionState, item: OOTItem) -> bool:
state._oot_stale[self.player] = True
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
state.prog_items[self.player][alt_item_name] += count
@@ -1313,8 +1316,12 @@ class OOTWorld(World):
state.prog_items[self.player][alt_item_name] -= count
if state.prog_items[self.player][alt_item_name] < 1:
del (state.prog_items[self.player][alt_item_name])
state._oot_stale[self.player] = True
return True
return super().remove(state, item)
changed = super().remove(state, item)
if changed:
state._oot_stale[self.player] = True
return changed
# Helper functions
@@ -1389,12 +1396,12 @@ class OOTWorld(World):
# If free_scarecrow give Scarecrow Song
if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True)
all_state.stale[self.player] = True
all_state._oot_stale[self.player] = True
return all_state
def get_filler_item_name(self) -> str:
return get_junk_item(count=1, pool=get_junk_pool(self))[0]
return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0]
def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool:
+35
View File
@@ -63,6 +63,7 @@ class MaxCombatLevel(Range):
The highest combat level of monster to possibly be assigned as a task.
If set to 0, no combat tasks will be generated.
"""
display_name = "Max Required Enemy Combat Level"
range_start = 0
range_end = 1520
default = 50
@@ -74,6 +75,7 @@ class MaxCombatTasks(Range):
If set to 0, no combat tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Combat Task Count"
range_start = 0
range_end = MAX_COMBAT_TASKS
default = MAX_COMBAT_TASKS
@@ -85,6 +87,7 @@ class CombatTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Combat Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -95,6 +98,7 @@ class MaxPrayerLevel(Range):
The highest Prayer requirement of any task generated.
If set to 0, no Prayer tasks will be generated.
"""
display_name = "Max Required Prayer Level"
range_start = 0
range_end = 99
default = 50
@@ -106,6 +110,7 @@ class MaxPrayerTasks(Range):
If set to 0, no Prayer tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Prayer Task Count"
range_start = 0
range_end = MAX_PRAYER_TASKS
default = MAX_PRAYER_TASKS
@@ -117,6 +122,7 @@ class PrayerTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Prayer Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -127,6 +133,7 @@ class MaxMagicLevel(Range):
The highest Magic requirement of any task generated.
If set to 0, no Magic tasks will be generated.
"""
display_name = "Max Required Magic Level"
range_start = 0
range_end = 99
default = 50
@@ -138,6 +145,7 @@ class MaxMagicTasks(Range):
If set to 0, no Magic tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Magic Task Count"
range_start = 0
range_end = MAX_MAGIC_TASKS
default = MAX_MAGIC_TASKS
@@ -149,6 +157,7 @@ class MagicTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Magic Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -159,6 +168,7 @@ class MaxRunecraftLevel(Range):
The highest Runecraft requirement of any task generated.
If set to 0, no Runecraft tasks will be generated.
"""
display_name = "Max Required Runecraft Level"
range_start = 0
range_end = 99
default = 50
@@ -170,6 +180,7 @@ class MaxRunecraftTasks(Range):
If set to 0, no Runecraft tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Runecraft Task Count"
range_start = 0
range_end = MAX_RUNECRAFT_TASKS
default = MAX_RUNECRAFT_TASKS
@@ -181,6 +192,7 @@ class RunecraftTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Runecraft Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -191,6 +203,7 @@ class MaxCraftingLevel(Range):
The highest Crafting requirement of any task generated.
If set to 0, no Crafting tasks will be generated.
"""
display_name = "Max Required Crafting Level"
range_start = 0
range_end = 99
default = 50
@@ -202,6 +215,7 @@ class MaxCraftingTasks(Range):
If set to 0, no Crafting tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Crafting Task Count"
range_start = 0
range_end = MAX_CRAFTING_TASKS
default = MAX_CRAFTING_TASKS
@@ -213,6 +227,7 @@ class CraftingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Crafting Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -223,6 +238,7 @@ class MaxMiningLevel(Range):
The highest Mining requirement of any task generated.
If set to 0, no Mining tasks will be generated.
"""
display_name = "Max Required Mining Level"
range_start = 0
range_end = 99
default = 50
@@ -234,6 +250,7 @@ class MaxMiningTasks(Range):
If set to 0, no Mining tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Mining Task Count"
range_start = 0
range_end = MAX_MINING_TASKS
default = MAX_MINING_TASKS
@@ -245,6 +262,7 @@ class MiningTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Mining Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -255,6 +273,7 @@ class MaxSmithingLevel(Range):
The highest Smithing requirement of any task generated.
If set to 0, no Smithing tasks will be generated.
"""
display_name = "Max Required Smithing Level"
range_start = 0
range_end = 99
default = 50
@@ -266,6 +285,7 @@ class MaxSmithingTasks(Range):
If set to 0, no Smithing tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Smithing Task Count"
range_start = 0
range_end = MAX_SMITHING_TASKS
default = MAX_SMITHING_TASKS
@@ -277,6 +297,7 @@ class SmithingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Smithing Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -287,6 +308,7 @@ class MaxFishingLevel(Range):
The highest Fishing requirement of any task generated.
If set to 0, no Fishing tasks will be generated.
"""
display_name = "Max Required Fishing Level"
range_start = 0
range_end = 99
default = 50
@@ -298,6 +320,7 @@ class MaxFishingTasks(Range):
If set to 0, no Fishing tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Fishing Task Count"
range_start = 0
range_end = MAX_FISHING_TASKS
default = MAX_FISHING_TASKS
@@ -309,6 +332,7 @@ class FishingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Fishing Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -319,6 +343,7 @@ class MaxCookingLevel(Range):
The highest Cooking requirement of any task generated.
If set to 0, no Cooking tasks will be generated.
"""
display_name = "Max Required Cooking Level"
range_start = 0
range_end = 99
default = 50
@@ -330,6 +355,7 @@ class MaxCookingTasks(Range):
If set to 0, no Cooking tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Cooking Task Count"
range_start = 0
range_end = MAX_COOKING_TASKS
default = MAX_COOKING_TASKS
@@ -341,6 +367,7 @@ class CookingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Cooking Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -351,6 +378,7 @@ class MaxFiremakingLevel(Range):
The highest Firemaking requirement of any task generated.
If set to 0, no Firemaking tasks will be generated.
"""
display_name = "Max Required Firemaking Level"
range_start = 0
range_end = 99
default = 50
@@ -362,6 +390,7 @@ class MaxFiremakingTasks(Range):
If set to 0, no Firemaking tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Firemaking Task Count"
range_start = 0
range_end = MAX_FIREMAKING_TASKS
default = MAX_FIREMAKING_TASKS
@@ -373,6 +402,7 @@ class FiremakingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Firemaking Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -383,6 +413,7 @@ class MaxWoodcuttingLevel(Range):
The highest Woodcutting requirement of any task generated.
If set to 0, no Woodcutting tasks will be generated.
"""
display_name = "Max Required Woodcutting Level"
range_start = 0
range_end = 99
default = 50
@@ -394,6 +425,7 @@ class MaxWoodcuttingTasks(Range):
If set to 0, no Woodcutting tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
display_name = "Max Woodcutting Task Count"
range_start = 0
range_end = MAX_WOODCUTTING_TASKS
default = MAX_WOODCUTTING_TASKS
@@ -405,6 +437,7 @@ class WoodcuttingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "Woodcutting Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -416,6 +449,7 @@ class MinimumGeneralTasks(Range):
General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so
there is no maximum.
"""
display_name = "Minimum General Task Count"
range_start = 0
range_end = NON_QUEST_LOCATION_COUNT
default = 10
@@ -427,6 +461,7 @@ class GeneralTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
display_name = "General Task Weight"
range_start = 0
range_end = 99
default = 50
+41 -17
View File
@@ -33,6 +33,12 @@ class OSRSWeb(WebWorld):
class OSRSWorld(World):
"""
The best retro fantasy MMORPG on the planet. Old School is RuneScape but older! This is the open world you know and love, but as it was in 2007.
The Randomizer takes the form of a Chunk-Restricted f2p Ironman that takes a brand new account up through defeating
the Green Dragon of Crandor and earning a spot in the fabled Champion's Guild!
"""
game = "Old School Runescape"
options_dataclass = OSRSOptions
options: OSRSOptions
@@ -84,16 +90,18 @@ class OSRSWorld(World):
rnd = self.random
starting_area = self.options.starting_area
#UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT
if not hasattr(self.multiworld, "generation_is_fake"):
if starting_area.value == StartingArea.option_any_bank:
self.starting_area_item = rnd.choice(starting_area_dict)
elif starting_area.value < StartingArea.option_chunksanity:
self.starting_area_item = starting_area_dict[starting_area.value]
else:
self.starting_area_item = rnd.choice(chunksanity_starting_chunks)
if starting_area.value == StartingArea.option_any_bank:
self.starting_area_item = rnd.choice(starting_area_dict)
elif starting_area.value < StartingArea.option_chunksanity:
self.starting_area_item = starting_area_dict[starting_area.value]
else:
self.starting_area_item = rnd.choice(chunksanity_starting_chunks)
# Set Starting Chunk
self.multiworld.push_precollected(self.create_item(self.starting_area_item))
# Set Starting Chunk
self.multiworld.push_precollected(self.create_item(self.starting_area_item))
"""
This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client.
@@ -103,8 +111,23 @@ class OSRSWorld(World):
def fill_slot_data(self):
data = self.options.as_dict("brutal_grinds")
data["data_csv_tag"] = data_csv_tag
data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv
return data
def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None:
if "starting_area" in slot_data:
self.starting_area_item = slot_data["starting_area"]
menu_region = self.multiworld.get_region("Menu",self.player)
menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
starting_area_region = self.starting_area_item[6:] # len("Area: ")
starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
def create_regions(self) -> None:
"""
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
@@ -122,13 +145,14 @@ class OSRSWorld(World):
# Removes the word "Area: " from the item name to get the region it applies to.
# I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
starting_area_region = self.starting_area_item[6:] # len("Area: ")
starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
starting_area_region = self.starting_area_item[6:] # len("Area: ")
starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
# Create entrances between regions
for region_row in region_rows:
@@ -635,7 +659,7 @@ class OSRSWorld(World):
else:
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
(can_gold(state) and can_smelt_gold(state))
if skill.lower() == "Cooking":
if skill.lower() == "cooking":
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \
state.can_reach(RegionNames.Egg, "Region", self.player) or \
+19
View File
@@ -1,3 +1,21 @@
# 2.3.0
### Features
- Added a Swedish translation of the setup guide.
- The client communicates map transitions to any trackers connected to the slot.
- Added the player's Normalize Encounter Rates option to slot data for trackers.
### Fixes
- Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if
the player randomized NPC gifts.
- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower.
- A Team Magma Grunt in the Space Center which could become unreachable while trainersanity is active by overlapping
with another NPC was moved to an unoccupied space.
- Fixed a problem where the client would crash on certain operating systems while using certain python versions if the
player tried to wonder trade.
# 2.2.0
### Features
@@ -175,6 +193,7 @@ turn to face you when you run.
species equally likely to appear, but makes rare encounters less rare.
- Added `Trick House` location group.
- Removed `Postgame Locations` location group.
- Added a Spanish translation of the setup guide.
### QoL
+19 -9
View File
@@ -133,6 +133,7 @@ class PokemonEmeraldClient(BizHawkClient):
latest_wonder_trade_reply: dict
wonder_trade_cooldown: int
wonder_trade_cooldown_timer: int
queued_received_trade: Optional[str]
death_counter: Optional[int]
previous_death_link: float
@@ -153,6 +154,7 @@ class PokemonEmeraldClient(BizHawkClient):
self.previous_death_link = 0
self.ignore_next_death_link = False
self.current_map = None
self.queued_received_trade = None
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
@@ -350,6 +352,7 @@ class PokemonEmeraldClient(BizHawkClient):
# Send game clear
if not ctx.finished_game and game_clear:
ctx.finished_game = True
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL,
@@ -548,22 +551,29 @@ class PokemonEmeraldClient(BizHawkClient):
(sb1_address + 0x37CC, [1], "System Bus"),
])
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
# Game is waiting on receiving a trade. See if there are any available trades that were not
# sent by this player, and if so, try to receive one.
if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data:
# Game is waiting on receiving a trade.
if self.queued_received_trade is not None:
# Client is holding a trade, ready to write it into the game
success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [
(sb1_address + 0x377C, json_to_pokemon_data(self.queued_received_trade), "System Bus"),
], [guards["SAVE BLOCK 1"]])
# Notify the player if it was written, otherwise hold it for the next loop
if success:
logger.info("Wonder trade received!")
self.queued_received_trade = None
elif self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data:
# See if there are any available trades that were not sent by this player. If so, try to receive one.
if any(item[0] != ctx.slot
for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items()
if key != "_lock" and orjson.loads(item[1])["species"] <= 386):
received_trade = await self.wonder_trade_receive(ctx)
if received_trade is None:
self.queued_received_trade = await self.wonder_trade_receive(ctx)
if self.queued_received_trade is None:
self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
self.wonder_trade_cooldown *= 2
self.wonder_trade_cooldown += random.randrange(0, 500)
else:
await bizhawk.write(ctx.bizhawk_ctx, [
(sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"),
])
logger.info("Wonder trade received!")
self.wonder_trade_cooldown = 5000
else:
+229 -187
View File
@@ -3,6 +3,7 @@ import settings
import typing
import threading
import base64
import random
from copy import deepcopy
from typing import TextIO
@@ -14,7 +15,7 @@ from worlds.generic.Rules import add_item_rule
from .items import item_table, item_groups
from .locations import location_data, PokemonRBLocation
from .regions import create_regions
from .options import pokemon_rb_options
from .options import PokemonRBOptions
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
@@ -71,7 +72,10 @@ class PokemonRedBlueWorld(World):
Elite Four to become the champion!"""
# -MuffinJets#4559
game = "Pokemon Red and Blue"
option_definitions = pokemon_rb_options
options_dataclass = PokemonRBOptions
options: PokemonRBOptions
settings: typing.ClassVar[PokemonSettings]
required_client_version = (0, 4, 2)
@@ -85,8 +89,8 @@ class PokemonRedBlueWorld(World):
web = PokemonWebWorld()
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.item_pool = []
self.total_key_items = None
self.fly_map = None
@@ -101,11 +105,11 @@ class PokemonRedBlueWorld(World):
self.learnsets = None
self.trainer_name = None
self.rival_name = None
self.type_chart = None
self.traps = None
self.trade_mons = {}
self.finished_level_scaling = threading.Event()
self.dexsanity_table = []
self.trainersanity_table = []
self.local_locs = []
@classmethod
@@ -113,11 +117,109 @@ class PokemonRedBlueWorld(World):
versions = set()
for player in multiworld.player_ids:
if multiworld.worlds[player].game == "Pokemon Red and Blue":
versions.add(multiworld.game_version[player].current_key)
versions.add(multiworld.worlds[player].options.game_version.current_key)
for version in versions:
if not os.path.exists(get_base_rom_path(version)):
raise FileNotFoundError(get_base_rom_path(version))
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld):
seed_groups = {}
pokemon_rb_worlds = multiworld.get_game_worlds("Pokemon Red and Blue")
for world in pokemon_rb_worlds:
if not (world.options.type_chart_seed.value.isdigit() or world.options.type_chart_seed.value == "random"):
seed_groups[world.options.type_chart_seed.value] = seed_groups.get(world.options.type_chart_seed.value,
[]) + [world]
copy_chart_worlds = {}
for worlds in seed_groups.values():
chosen_world = multiworld.random.choice(worlds)
for world in worlds:
if world is not chosen_world:
copy_chart_worlds[world.player] = chosen_world
for world in pokemon_rb_worlds:
if world.player in copy_chart_worlds:
continue
tc_random = world.random
if world.options.type_chart_seed.value.isdigit():
tc_random = random.Random()
tc_random.seed(int(world.options.type_chart_seed.value))
if world.options.randomize_type_chart == "vanilla":
chart = deepcopy(poke_data.type_chart)
elif world.options.randomize_type_chart == "randomize":
types = poke_data.type_names.values()
matchups = []
for type1 in types:
for type2 in types:
matchups.append([type1, type2])
tc_random.shuffle(matchups)
immunities = world.options.immunity_matchups.value
super_effectives = world.options.super_effective_matchups.value
not_very_effectives = world.options.not_very_effective_matchups.value
normals = world.options.normal_matchups.value
while super_effectives + not_very_effectives + normals < 225 - immunities:
if super_effectives == not_very_effectives == normals == 0:
super_effectives = 225
not_very_effectives = 225
normals = 225
else:
super_effectives += world.options.super_effective_matchups.value
not_very_effectives += world.options.not_very_effective_matchups.value
normals += world.options.normal_matchups.value
if super_effectives + not_very_effectives + normals > 225 - immunities:
total = super_effectives + not_very_effectives + normals
excess = total - (225 - immunities)
subtract_amounts = (
int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives),
int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives),
int((excess / (super_effectives + not_very_effectives + normals)) * normals))
super_effectives -= subtract_amounts[0]
not_very_effectives -= subtract_amounts[1]
normals -= subtract_amounts[2]
while super_effectives + not_very_effectives + normals > 225 - immunities:
r = tc_random.randint(0, 2)
if r == 0 and super_effectives:
super_effectives -= 1
elif r == 1 and not_very_effectives:
not_very_effectives -= 1
elif normals:
normals -= 1
chart = []
for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives],
[0, 10, 20, 5]):
for _ in range(matchup_list):
matchup = matchups.pop()
matchup.append(matchup_value)
chart.append(matchup)
elif world.options.randomize_type_chart == "chaos":
types = poke_data.type_names.values()
matchups = []
for type1 in types:
for type2 in types:
matchups.append([type1, type2])
chart = []
values = list(range(21))
tc_random.shuffle(matchups)
tc_random.shuffle(values)
for matchup in matchups:
value = values.pop(0)
values.append(value)
matchup.append(value)
chart.append(matchup)
# sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective"
# matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to
# damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
# to the way effectiveness messages are generated.
world.type_chart = sorted(chart, key=lambda matchup: -matchup[2])
for player in copy_chart_worlds:
multiworld.worlds[player].type_chart = copy_chart_worlds[player].type_chart
def generate_early(self):
def encode_name(name, t):
try:
@@ -126,33 +228,33 @@ class PokemonRedBlueWorld(World):
return encode_text(name, length=8, whitespace="@", safety=True)
except KeyError as e:
raise KeyError(f"Invalid character(s) in {t} name for player {self.multiworld.player_name[self.player]}") from e
if self.multiworld.trainer_name[self.player] == "choose_in_game":
if self.options.trainer_name == "choose_in_game":
self.trainer_name = "choose_in_game"
else:
self.trainer_name = encode_name(self.multiworld.trainer_name[self.player].value, "Player")
if self.multiworld.rival_name[self.player] == "choose_in_game":
self.trainer_name = encode_name(self.options.trainer_name.value, "Player")
if self.options.rival_name == "choose_in_game":
self.rival_name = "choose_in_game"
else:
self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival")
self.rival_name = encode_name(self.options.rival_name.value, "Rival")
if not self.multiworld.badgesanity[self.player]:
self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"]
if not self.options.badgesanity:
self.options.non_local_items.value -= self.item_name_groups["Badges"]
if self.multiworld.key_items_only[self.player]:
self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off")
self.multiworld.dexsanity[self.player].value = 0
self.multiworld.randomize_hidden_items[self.player] = \
self.multiworld.randomize_hidden_items[self.player].from_text("off")
if self.options.key_items_only:
self.options.trainersanity.value = 0
self.options.dexsanity.value = 0
self.options.randomize_hidden_items = \
self.options.randomize_hidden_items.from_text("off")
if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2:
if self.options.badges_needed_for_hm_moves.value >= 2:
badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3:
if self.options.badges_needed_for_hm_moves.value == 3:
badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
"Soul Badge", "Volcano Badge", "Earth Badge"]
self.multiworld.random.shuffle(badges)
self.random.shuffle(badges)
badges_to_add += [badges.pop(), badges.pop()]
hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"]
self.multiworld.random.shuffle(hm_moves)
self.random.shuffle(hm_moves)
self.extra_badges = {}
for badge in badges_to_add:
self.extra_badges[hm_moves.pop()] = badge
@@ -160,79 +262,17 @@ class PokemonRedBlueWorld(World):
process_move_data(self)
process_pokemon_data(self)
if self.multiworld.randomize_type_chart[self.player] == "vanilla":
chart = deepcopy(poke_data.type_chart)
elif self.multiworld.randomize_type_chart[self.player] == "randomize":
types = poke_data.type_names.values()
matchups = []
for type1 in types:
for type2 in types:
matchups.append([type1, type2])
self.multiworld.random.shuffle(matchups)
immunities = self.multiworld.immunity_matchups[self.player].value
super_effectives = self.multiworld.super_effective_matchups[self.player].value
not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value
normals = self.multiworld.normal_matchups[self.player].value
while super_effectives + not_very_effectives + normals < 225 - immunities:
if super_effectives == not_very_effectives == normals == 0:
super_effectives = 225
not_very_effectives = 225
normals = 225
else:
super_effectives += self.multiworld.super_effective_matchups[self.player].value
not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value
normals += self.multiworld.normal_matchups[self.player].value
if super_effectives + not_very_effectives + normals > 225 - immunities:
total = super_effectives + not_very_effectives + normals
excess = total - (225 - immunities)
subtract_amounts = (
int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives),
int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives),
int((excess / (super_effectives + not_very_effectives + normals)) * normals))
super_effectives -= subtract_amounts[0]
not_very_effectives -= subtract_amounts[1]
normals -= subtract_amounts[2]
while super_effectives + not_very_effectives + normals > 225 - immunities:
r = self.multiworld.random.randint(0, 2)
if r == 0 and super_effectives:
super_effectives -= 1
elif r == 1 and not_very_effectives:
not_very_effectives -= 1
elif normals:
normals -= 1
chart = []
for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives],
[0, 10, 20, 5]):
for _ in range(matchup_list):
matchup = matchups.pop()
matchup.append(matchup_value)
chart.append(matchup)
elif self.multiworld.randomize_type_chart[self.player] == "chaos":
types = poke_data.type_names.values()
matchups = []
for type1 in types:
for type2 in types:
matchups.append([type1, type2])
chart = []
values = list(range(21))
self.multiworld.random.shuffle(matchups)
self.multiworld.random.shuffle(values)
for matchup in matchups:
value = values.pop(0)
values.append(value)
matchup.append(value)
chart.append(matchup)
# sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective"
# matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to
# damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
# to the way effectiveness messages are generated.
self.type_chart = sorted(chart, key=lambda matchup: -matchup[2])
self.dexsanity_table = [
*(True for _ in range(round(self.multiworld.dexsanity[self.player].value * 1.51))),
*(False for _ in range(151 - round(self.multiworld.dexsanity[self.player].value * 1.51)))
*(True for _ in range(round(self.options.dexsanity.value))),
*(False for _ in range(151 - round(self.options.dexsanity.value)))
]
self.multiworld.random.shuffle(self.dexsanity_table)
self.random.shuffle(self.dexsanity_table)
self.trainersanity_table = [
*(True for _ in range(self.options.trainersanity.value)),
*(False for _ in range(317 - self.options.trainersanity.value))
]
self.random.shuffle(self.trainersanity_table)
def create_items(self):
self.multiworld.itempool += self.item_pool
@@ -275,9 +315,9 @@ class PokemonRedBlueWorld(World):
filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)]
def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
if not self.multiworld.badgesanity[self.player]:
if not self.options.badgesanity:
# Door Shuffle options besides Simple place badges during door shuffling
if self.multiworld.door_shuffle[self.player] in ("off", "simple"):
if self.options.door_shuffle in ("off", "simple"):
badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
for badge in badges:
self.multiworld.itempool.remove(badge)
@@ -297,8 +337,8 @@ class PokemonRedBlueWorld(World):
for mon in poke_data.pokemon_data.keys():
state.collect(self.create_item(mon), True)
state.sweep_for_advancements()
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
self.random.shuffle(badges)
self.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()
# allow_partial so that unplaced badges aren't lost, for debugging purposes
fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True)
@@ -318,7 +358,7 @@ class PokemonRedBlueWorld(World):
raise FillError(f"Failed to place badges for player {self.player}")
verify_hm_moves(self.multiworld, self, self.player)
if self.multiworld.key_items_only[self.player]:
if self.options.key_items_only:
return
tms = [item for item in usefulitempool + filleritempool if item.name.startswith("TM") and (item.player ==
@@ -340,7 +380,7 @@ class PokemonRedBlueWorld(World):
int((int(tm.name[2:4]) - 1) / 8)] & 1 << ((int(tm.name[2:4]) - 1) % 8)]
if not learnable_tms:
learnable_tms = tms
tm = self.multiworld.random.choice(learnable_tms)
tm = self.random.choice(learnable_tms)
loc.place_locked_item(tm)
fill_locations.remove(loc)
@@ -370,9 +410,9 @@ class PokemonRedBlueWorld(World):
if not all_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
if self.multiworld.old_man[self.player] == "early_parcel":
if self.options.old_man == "early_parcel":
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
if self.multiworld.dexsanity[self.player]:
if self.options.dexsanity:
for i, mon in enumerate(poke_data.pokemon_data):
if self.dexsanity_table[i]:
location = self.multiworld.get_location(f"Pokedex - {mon}", self.player)
@@ -384,13 +424,13 @@ class PokemonRedBlueWorld(World):
locs = {self.multiworld.get_location("Fossil - Choice A", self.player),
self.multiworld.get_location("Fossil - Choice B", self.player)}
if not self.multiworld.key_items_only[self.player]:
if not self.options.key_items_only:
rule = None
if self.multiworld.fossil_check_item_types[self.player] == "key_items":
if self.options.fossil_check_item_types == "key_items":
rule = lambda i: i.advancement
elif self.multiworld.fossil_check_item_types[self.player] == "unique_items":
elif self.options.fossil_check_item_types == "unique_items":
rule = lambda i: i.name in item_groups["Unique"]
elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items":
elif self.options.fossil_check_item_types == "no_key_items":
rule = lambda i: not i.advancement
if rule:
for loc in locs:
@@ -406,16 +446,16 @@ class PokemonRedBlueWorld(World):
if loc.item is None:
locs.add(loc)
if not self.multiworld.key_items_only[self.player]:
if not self.options.key_items_only:
loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
if loc.item is None:
locs.add(loc)
for loc in sorted(locs):
if loc.name in self.multiworld.priority_locations[self.player].value:
if loc.name in self.options.priority_locations.value:
add_item_rule(loc, lambda i: i.advancement)
add_item_rule(loc, lambda i: i.player == self.player)
if self.multiworld.old_man[self.player] == "early_parcel" and loc.name != "Player's House 2F - Player's PC":
if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC":
add_item_rule(loc, lambda i: i.name != "Oak's Parcel")
self.local_locs = locs
@@ -440,10 +480,10 @@ class PokemonRedBlueWorld(World):
else:
region_mons.add(location.item.name)
self.multiworld.elite_four_pokedex_condition[self.player].total = \
int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value)
self.options.elite_four_pokedex_condition.total = \
int((len(reachable_mons) / 100) * self.options.elite_four_pokedex_condition.value)
if self.multiworld.accessibility[self.player] == "full":
if self.options.accessibility == "full":
balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]]
traps = [self.create_item(trap) for trap in item_groups["Traps"]]
locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in
@@ -469,7 +509,7 @@ class PokemonRedBlueWorld(World):
else:
break
else:
self.multiworld.random.shuffle(traps)
self.random.shuffle(traps)
for trap in traps:
try:
self.multiworld.itempool.remove(trap)
@@ -497,22 +537,22 @@ class PokemonRedBlueWorld(World):
found_mons.add(key)
def create_regions(self):
if (self.multiworld.old_man[self.player] == "vanilla" or
self.multiworld.door_shuffle[self.player] in ("full", "insanity")):
fly_map_codes = self.multiworld.random.sample(range(2, 11), 2)
elif (self.multiworld.door_shuffle[self.player] == "simple" or
self.multiworld.route_3_condition[self.player] == "boulder_badge" or
(self.multiworld.route_3_condition[self.player] == "any_badge" and
self.multiworld.badgesanity[self.player])):
fly_map_codes = self.multiworld.random.sample(range(3, 11), 2)
if (self.options.old_man == "vanilla" or
self.options.door_shuffle in ("full", "insanity")):
fly_map_codes = self.random.sample(range(2, 11), 2)
elif (self.options.door_shuffle == "simple" or
self.options.route_3_condition == "boulder_badge" or
(self.options.route_3_condition == "any_badge" and
self.options.badgesanity)):
fly_map_codes = self.random.sample(range(3, 11), 2)
else:
fly_map_codes = self.multiworld.random.sample([4, 6, 7, 8, 9, 10], 2)
if self.multiworld.free_fly_location[self.player]:
fly_map_codes = self.random.sample([4, 6, 7, 8, 9, 10], 2)
if self.options.free_fly_location:
fly_map_code = fly_map_codes[0]
else:
fly_map_code = 0
if self.multiworld.town_map_fly_location[self.player]:
if self.options.town_map_fly_location:
town_map_fly_map_code = fly_map_codes[1]
else:
town_map_fly_map_code = 0
@@ -528,7 +568,7 @@ class PokemonRedBlueWorld(World):
self.multiworld.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player)
def set_rules(self):
set_rules(self.multiworld, self.player)
set_rules(self.multiworld, self, self.player)
def create_item(self, name: str) -> Item:
return PokemonRBItem(name, self.player)
@@ -548,19 +588,19 @@ class PokemonRedBlueWorld(World):
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
def write_spoiler_header(self, spoiler_handle: TextIO):
spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n")
spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n")
spoiler_handle.write(f"Elite Four Total Pokemon: {self.multiworld.elite_four_pokedex_condition[self.player].total}\n")
if self.multiworld.free_fly_location[self.player]:
spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.options.cerulean_cave_key_items_condition.total}\n")
spoiler_handle.write(f"Elite Four Total Key Items: {self.options.elite_four_key_items_condition.total}\n")
spoiler_handle.write(f"Elite Four Total Pokemon: {self.options.elite_four_pokedex_condition.total}\n")
if self.options.free_fly_location:
spoiler_handle.write(f"Free Fly Location: {self.fly_map}\n")
if self.multiworld.town_map_fly_location[self.player]:
if self.options.town_map_fly_location:
spoiler_handle.write(f"Town Map Fly Location: {self.town_map_fly_map}\n")
if self.extra_badges:
for hm_move, badge in self.extra_badges.items():
spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n")
def write_spoiler(self, spoiler_handle):
if self.multiworld.randomize_type_chart[self.player].value:
if self.options.randomize_type_chart:
spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n")
for matchup in self.type_chart:
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
@@ -571,39 +611,39 @@ class PokemonRedBlueWorld(World):
spoiler_handle.write(location.name + ": " + location.item.name + "\n")
def get_filler_item_name(self) -> str:
combined_traps = (self.multiworld.poison_trap_weight[self.player].value
+ self.multiworld.fire_trap_weight[self.player].value
+ self.multiworld.paralyze_trap_weight[self.player].value
+ self.multiworld.ice_trap_weight[self.player].value
+ self.multiworld.sleep_trap_weight[self.player].value)
combined_traps = (self.options.poison_trap_weight.value
+ self.options.fire_trap_weight.value
+ self.options.paralyze_trap_weight.value
+ self.options.ice_trap_weight.value
+ self.options.sleep_trap_weight.value)
if (combined_traps > 0 and
self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value):
self.random.randint(1, 100) <= self.options.trap_percentage.value):
return self.select_trap()
banned_items = item_groups["Unique"]
if (((not self.multiworld.tea[self.player]) or "Saffron City" not in [self.fly_map, self.town_map_fly_map])
and (not self.multiworld.door_shuffle[self.player])):
if (((not self.options.tea) or "Saffron City" not in [self.fly_map, self.town_map_fly_map])
and (not self.options.door_shuffle)):
# under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being
# able to reach the Celadon Department Store, so Poké Dolls would not allow early access to anything
banned_items.append("Poke Doll")
if not self.multiworld.tea[self.player]:
if not self.options.tea:
banned_items += item_groups["Vending Machine Drinks"]
return self.multiworld.random.choice([item for item in item_table if item_table[item].id and item_table[
return self.random.choice([item for item in item_table if item_table[item].id and item_table[
item].classification == ItemClassification.filler and item not in banned_items])
def select_trap(self):
if self.traps is None:
self.traps = []
self.traps += ["Poison Trap"] * self.multiworld.poison_trap_weight[self.player].value
self.traps += ["Fire Trap"] * self.multiworld.fire_trap_weight[self.player].value
self.traps += ["Paralyze Trap"] * self.multiworld.paralyze_trap_weight[self.player].value
self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value
self.traps += ["Sleep Trap"] * self.multiworld.sleep_trap_weight[self.player].value
return self.multiworld.random.choice(self.traps)
self.traps += ["Poison Trap"] * self.options.poison_trap_weight.value
self.traps += ["Fire Trap"] * self.options.fire_trap_weight.value
self.traps += ["Paralyze Trap"] * self.options.paralyze_trap_weight.value
self.traps += ["Ice Trap"] * self.options.ice_trap_weight.value
self.traps += ["Sleep Trap"] * self.options.sleep_trap_weight.value
return self.random.choice(self.traps)
def extend_hint_information(self, hint_data):
if self.multiworld.dexsanity[self.player] or self.multiworld.door_shuffle[self.player]:
if self.options.dexsanity or self.options.door_shuffle:
hint_data[self.player] = {}
if self.multiworld.dexsanity[self.player]:
if self.options.dexsanity:
mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()}
for loc in location_data:
if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]:
@@ -616,57 +656,59 @@ class PokemonRedBlueWorld(World):
hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] =\
", ".join(mon_locations[mon])
if self.multiworld.door_shuffle[self.player]:
if self.options.door_shuffle:
for location in self.multiworld.get_locations(self.player):
if location.parent_region.entrance_hint and location.address:
hint_data[self.player][location.address] = location.parent_region.entrance_hint
def fill_slot_data(self) -> dict:
return {
"second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value,
"require_item_finder": self.multiworld.require_item_finder[self.player].value,
"randomize_hidden_items": self.multiworld.randomize_hidden_items[self.player].value,
"badges_needed_for_hm_moves": self.multiworld.badges_needed_for_hm_moves[self.player].value,
"oaks_aide_rt_2": self.multiworld.oaks_aide_rt_2[self.player].value,
"oaks_aide_rt_11": self.multiworld.oaks_aide_rt_11[self.player].value,
"oaks_aide_rt_15": self.multiworld.oaks_aide_rt_15[self.player].value,
"extra_key_items": self.multiworld.extra_key_items[self.player].value,
"extra_strength_boulders": self.multiworld.extra_strength_boulders[self.player].value,
"tea": self.multiworld.tea[self.player].value,
"old_man": self.multiworld.old_man[self.player].value,
"elite_four_badges_condition": self.multiworld.elite_four_badges_condition[self.player].value,
"elite_four_key_items_condition": self.multiworld.elite_four_key_items_condition[self.player].total,
"elite_four_pokedex_condition": self.multiworld.elite_four_pokedex_condition[self.player].total,
"victory_road_condition": self.multiworld.victory_road_condition[self.player].value,
"route_22_gate_condition": self.multiworld.route_22_gate_condition[self.player].value,
"route_3_condition": self.multiworld.route_3_condition[self.player].value,
"robbed_house_officer": self.multiworld.robbed_house_officer[self.player].value,
"viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value,
"cerulean_cave_badges_condition": self.multiworld.cerulean_cave_badges_condition[self.player].value,
"cerulean_cave_key_items_condition": self.multiworld.cerulean_cave_key_items_condition[self.player].total,
ret = {
"second_fossil_check_condition": self.options.second_fossil_check_condition.value,
"require_item_finder": self.options.require_item_finder.value,
"randomize_hidden_items": self.options.randomize_hidden_items.value,
"badges_needed_for_hm_moves": self.options.badges_needed_for_hm_moves.value,
"oaks_aide_rt_2": self.options.oaks_aide_rt_2.value,
"oaks_aide_rt_11": self.options.oaks_aide_rt_11.value,
"oaks_aide_rt_15": self.options.oaks_aide_rt_15.value,
"extra_key_items": self.options.extra_key_items.value,
"extra_strength_boulders": self.options.extra_strength_boulders.value,
"tea": self.options.tea.value,
"old_man": self.options.old_man.value,
"elite_four_badges_condition": self.options.elite_four_badges_condition.value,
"elite_four_key_items_condition": self.options.elite_four_key_items_condition.total,
"elite_four_pokedex_condition": self.options.elite_four_pokedex_condition.total,
"victory_road_condition": self.options.victory_road_condition.value,
"route_22_gate_condition": self.options.route_22_gate_condition.value,
"route_3_condition": self.options.route_3_condition.value,
"robbed_house_officer": self.options.robbed_house_officer.value,
"viridian_gym_condition": self.options.viridian_gym_condition.value,
"cerulean_cave_badges_condition": self.options.cerulean_cave_badges_condition.value,
"cerulean_cave_key_items_condition": self.options.cerulean_cave_key_items_condition.total,
"free_fly_map": self.fly_map_code,
"town_map_fly_map": self.town_map_fly_map_code,
"extra_badges": self.extra_badges,
"type_chart": self.type_chart,
"randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value,
"trainersanity": self.multiworld.trainersanity[self.player].value,
"death_link": self.multiworld.death_link[self.player].value,
"prizesanity": self.multiworld.prizesanity[self.player].value,
"key_items_only": self.multiworld.key_items_only[self.player].value,
"poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value,
"bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value,
"stonesanity": self.multiworld.stonesanity[self.player].value,
"door_shuffle": self.multiworld.door_shuffle[self.player].value,
"warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value,
"dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value,
"split_card_key": self.multiworld.split_card_key[self.player].value,
"all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value,
"require_pokedex": self.multiworld.require_pokedex[self.player].value,
"area_1_to_1_mapping": self.multiworld.area_1_to_1_mapping[self.player].value,
"blind_trainers": self.multiworld.blind_trainers[self.player].value,
"randomize_pokedex": self.options.randomize_pokedex.value,
"trainersanity": self.options.trainersanity.value,
"death_link": self.options.death_link.value,
"prizesanity": self.options.prizesanity.value,
"key_items_only": self.options.key_items_only.value,
"poke_doll_skip": self.options.poke_doll_skip.value,
"bicycle_gate_skips": self.options.bicycle_gate_skips.value,
"stonesanity": self.options.stonesanity.value,
"door_shuffle": self.options.door_shuffle.value,
"warp_tile_shuffle": self.options.warp_tile_shuffle.value,
"dark_rock_tunnel_logic": self.options.dark_rock_tunnel_logic.value,
"split_card_key": self.options.split_card_key.value,
"all_elevators_locked": self.options.all_elevators_locked.value,
"require_pokedex": self.options.require_pokedex.value,
"area_1_to_1_mapping": self.options.area_1_to_1_mapping.value,
"blind_trainers": self.options.blind_trainers.value,
}
if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit():
ret["type_chart"] = self.type_chart
return ret
class PokemonRBItem(Item):
game = "Pokemon Red and Blue"
Binary file not shown.
Binary file not shown.
@@ -60,11 +60,12 @@ and Safari Zone. Adds 4 extra item locations to Rock Tunnel B1F
* Split Card Key: Splits the Card Key into 10 different Card Keys, one for each floor of Silph Co that has locked doors.
Adds 9 location checks to friendly NPCs in Silph Co. You can also choose Progressive Card Keys to always obtain the
keys in order from Card Key 2F to Card Key 11F.
* Trainersanity: Adds location checks to 317 trainers. Does not include scripted trainers, most of which disappear
* Trainersanity: Adds location checks to trainers. You may choose between 0 and 317 trainersanity checks. Trainers
will be randomly selected to be given checks. Does not include scripted trainers, most of which disappear
after battling them, but also includes Gym Leaders. You must talk to the trainer after defeating them to receive
your prize. Adds 317 random filler items to the item pool
* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose a percentage
of Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty
your prize. Adds random filler items to the item pool.
* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose between 0 and 151
Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty
Poké Ball icon shown in battle or in the Pokédex menu.
## Which items can be in another player's world?
+50 -50
View File
@@ -8,7 +8,7 @@ def get_encounter_slots(self):
for location in encounter_slots:
if isinstance(location.original_item, list):
location.original_item = location.original_item[not self.multiworld.game_version[self.player].value]
location.original_item = location.original_item[not self.options.game_version.value]
return encounter_slots
@@ -39,16 +39,16 @@ def randomize_pokemon(self, mon, mons_list, randomize_type, random):
return mon
def process_trainer_data(self):
def process_trainer_data(world):
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.multiworld.trainer_legendaries[self.player].value]
or world.options.trainer_legendaries.value]
unevolved_mons = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon
or self.multiworld.randomize_legendary_pokemon[self.player].value == 3]
or world.options.randomize_legendary_pokemon.value == 3]
evolved_mons = [mon for mon in mons_list if mon not in unevolved_mons]
rival_map = {
"Charmander": self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name[9:], # strip the
"Squirtle": self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name[9:], # 'Missable'
"Bulbasaur": self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name[9:], # from the name
"Charmander": world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name[9:], # strip the
"Squirtle": world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name[9:], # 'Missable'
"Bulbasaur": world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name[9:], # from the name
}
def add_evolutions():
@@ -60,7 +60,7 @@ def process_trainer_data(self):
rival_map[poke_data.evolves_to[a]] = b
add_evolutions()
add_evolutions()
parties_objs = [location for location in self.multiworld.get_locations(self.player)
parties_objs = [location for location in world.multiworld.get_locations(world.player)
if location.type == "Trainer Parties"]
# Process Rival parties in order "Route 22 " is not a typo
parties_objs.sort(key=lambda i: 0 if "Oak's Lab" in i.name else 1 if "Route 22 " in i.name else 2 if "Cerulean City"
@@ -75,25 +75,25 @@ def process_trainer_data(self):
for i, mon in enumerate(rival_party):
if mon in ("Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard",
"Squirtle", "Wartortle", "Blastoise"):
if self.multiworld.randomize_starter_pokemon[self.player]:
if world.options.randomize_starter_pokemon:
rival_party[i] = rival_map[mon]
elif self.multiworld.randomize_trainer_parties[self.player]:
elif world.options.randomize_trainer_parties:
if mon in rival_map:
rival_party[i] = rival_map[mon]
else:
new_mon = randomize_pokemon(self, mon,
new_mon = randomize_pokemon(world, mon,
unevolved_mons if mon in unevolved_mons else evolved_mons,
self.multiworld.randomize_trainer_parties[self.player].value,
self.multiworld.random)
world.options.randomize_trainer_parties.value,
world.random)
rival_map[mon] = new_mon
rival_party[i] = new_mon
add_evolutions()
else:
if self.multiworld.randomize_trainer_parties[self.player]:
if world.options.randomize_trainer_parties:
for i, mon in enumerate(party["party"]):
party["party"][i] = randomize_pokemon(self, mon, mons_list,
self.multiworld.randomize_trainer_parties[self.player].value,
self.multiworld.random)
party["party"][i] = randomize_pokemon(world, mon, mons_list,
world.options.randomize_trainer_parties.value,
world.random)
def process_pokemon_locations(self):
@@ -106,21 +106,21 @@ def process_pokemon_locations(self):
placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()}
mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon
or self.multiworld.randomize_legendary_pokemon[self.player].value == 3]
if self.multiworld.randomize_legendary_pokemon[self.player] == "vanilla":
or self.options.randomize_legendary_pokemon.value == 3]
if self.options.randomize_legendary_pokemon == "vanilla":
for slot in legendary_slots:
location = self.multiworld.get_location(slot.name, self.player)
location.place_locked_item(self.create_item("Static " + slot.original_item))
elif self.multiworld.randomize_legendary_pokemon[self.player] == "shuffle":
self.multiworld.random.shuffle(legendary_mons)
elif self.options.randomize_legendary_pokemon == "shuffle":
self.random.shuffle(legendary_mons)
for slot in legendary_slots:
location = self.multiworld.get_location(slot.name, self.player)
mon = legendary_mons.pop()
location.place_locked_item(self.create_item("Static " + mon))
placed_mons[mon] += 1
elif self.multiworld.randomize_legendary_pokemon[self.player] == "static":
elif self.options.randomize_legendary_pokemon == "static":
static_slots = static_slots + legendary_slots
self.multiworld.random.shuffle(static_slots)
self.random.shuffle(static_slots)
static_slots.sort(key=lambda s: s.name != "Pokemon Tower 6F - Restless Soul")
while legendary_slots:
swap_slot = legendary_slots.pop()
@@ -131,12 +131,12 @@ def process_pokemon_locations(self):
location = self.multiworld.get_location(slot.name, self.player)
location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item))
swap_slot.original_item = slot.original_item
elif self.multiworld.randomize_legendary_pokemon[self.player] == "any":
elif self.options.randomize_legendary_pokemon == "any":
static_slots = static_slots + legendary_slots
for slot in static_slots:
location = self.multiworld.get_location(slot.name, self.player)
randomize_type = self.multiworld.randomize_static_pokemon[self.player].value
randomize_type = self.options.randomize_static_pokemon.value
slot_type = slot.type.split()[0]
if slot_type == "Legendary":
slot_type = "Static"
@@ -145,7 +145,7 @@ def process_pokemon_locations(self):
else:
mon = self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type,
self.multiworld.random))
self.random))
location.place_locked_item(mon)
if slot_type != "Missable":
placed_mons[mon.name.replace("Static ", "")] += 1
@@ -153,16 +153,16 @@ def process_pokemon_locations(self):
chosen_mons = set()
for slot in starter_slots:
location = self.multiworld.get_location(slot.name, self.player)
randomize_type = self.multiworld.randomize_starter_pokemon[self.player].value
randomize_type = self.options.randomize_starter_pokemon.value
slot_type = "Missable"
if not randomize_type:
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list,
randomize_type, self.multiworld.random))
randomize_type, self.random))
while mon.name in chosen_mons:
mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list,
randomize_type, self.multiworld.random))
randomize_type, self.random))
chosen_mons.add(mon.name)
location.place_locked_item(mon)
@@ -170,10 +170,10 @@ def process_pokemon_locations(self):
encounter_slots = encounter_slots_master.copy()
zone_mapping = {}
if self.multiworld.randomize_wild_pokemon[self.player]:
if self.options.randomize_wild_pokemon:
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.multiworld.randomize_legendary_pokemon[self.player].value == 3]
self.multiworld.random.shuffle(encounter_slots)
or self.options.randomize_legendary_pokemon.value == 3]
self.random.shuffle(encounter_slots)
locations = []
for slot in encounter_slots:
location = self.multiworld.get_location(slot.name, self.player)
@@ -181,11 +181,11 @@ def process_pokemon_locations(self):
if zone not in zone_mapping:
zone_mapping[zone] = {}
original_mon = slot.original_item
if self.multiworld.area_1_to_1_mapping[self.player] and original_mon in zone_mapping[zone]:
if self.options.area_1_to_1_mapping and original_mon in zone_mapping[zone]:
mon = zone_mapping[zone][original_mon]
else:
mon = randomize_pokemon(self, original_mon, mons_list,
self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random)
self.options.randomize_wild_pokemon.value, self.random)
#
while ("Pokemon Tower 6F" in slot.name and
self.multiworld.get_location("Pokemon Tower 6F - Restless Soul", self.player).item.name
@@ -194,7 +194,7 @@ def process_pokemon_locations(self):
# the battle is treates as the Restless Soul battle and you cannot catch it. So, prevent any wild mons
# from being the same species as the Restless Soul.
# to account for the possibility that only one ground type Pokemon exists, match only stats for this fix
mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random)
mon = randomize_pokemon(self, original_mon, mons_list, 2, self.random)
placed_mons[mon] += 1
location.item = self.create_item(mon)
location.locked = True
@@ -204,28 +204,28 @@ def process_pokemon_locations(self):
mons_to_add = []
remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and
(pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)]
if self.multiworld.catch_em_all[self.player] == "first_stage":
(pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)]
if self.options.catch_em_all == "first_stage":
mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and
(pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)]
elif self.multiworld.catch_em_all[self.player] == "all_pokemon":
(pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)]
elif self.options.catch_em_all == "all_pokemon":
mons_to_add = remaining_pokemon.copy()
logic_needed_mons = max(self.multiworld.oaks_aide_rt_2[self.player].value,
self.multiworld.oaks_aide_rt_11[self.player].value,
self.multiworld.oaks_aide_rt_15[self.player].value)
if self.multiworld.accessibility[self.player] == "minimal":
logic_needed_mons = max(self.options.oaks_aide_rt_2.value,
self.options.oaks_aide_rt_11.value,
self.options.oaks_aide_rt_15.value)
if self.options.accessibility == "minimal":
logic_needed_mons = 0
self.multiworld.random.shuffle(remaining_pokemon)
self.random.shuffle(remaining_pokemon)
while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0])
+ len(mons_to_add) < logic_needed_mons):
mons_to_add.append(remaining_pokemon.pop())
for mon in mons_to_add:
stat_base = get_base_stat_total(mon)
candidate_locations = encounter_slots_master.copy()
if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]:
if self.options.randomize_wild_pokemon.current_key in ["match_base_stats", "match_types_and_base_stats"]:
candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.original_item) - stat_base))
if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]:
if self.options.randomize_wild_pokemon.current_key in ["match_types", "match_types_and_base_stats"]:
candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in
[self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]],
poke_data.pokemon_data[slot.original_item]["type2"] in
@@ -233,12 +233,12 @@ def process_pokemon_locations(self):
candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations]
for location in candidate_locations:
zone = " - ".join(location.name.split(" - ")[:-1])
if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]:
if self.options.catch_em_all == "all_pokemon" and self.options.area_1_to_1_mapping:
if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master
if (not l.name.startswith(zone)) and
self.multiworld.get_location(l.name, self.player).item.name == location.item.name]:
continue
if self.multiworld.catch_em_all[self.player] == "first_stage" and self.multiworld.area_1_to_1_mapping[self.player]:
if self.options.catch_em_all == "first_stage" and self.options.area_1_to_1_mapping:
if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master
if (not l.name.startswith(zone)) and
self.multiworld.get_location(l.name, self.player).item.name == location.item.name and l.name
@@ -246,10 +246,10 @@ def process_pokemon_locations(self):
continue
if placed_mons[location.item.name] < 2 and (location.item.name in poke_data.first_stage_pokemon
or self.multiworld.catch_em_all[self.player]):
or self.options.catch_em_all):
continue
if self.multiworld.area_1_to_1_mapping[self.player]:
if self.options.area_1_to_1_mapping:
place_locations = [place_location for place_location in candidate_locations if
place_location.name.startswith(zone) and
place_location.item.name == location.item.name]
+2
View File
@@ -194,6 +194,8 @@ item_table = {
"Fuji Saved": ItemData(None, ItemClassification.progression, []),
"Silph Co Liberated": ItemData(None, ItemClassification.progression, []),
"Become Champion": ItemData(None, ItemClassification.progression, []),
"Mt Moon Fossils": ItemData(None, ItemClassification.progression, []),
"Cinnabar Lab": ItemData(None, ItemClassification.progression, []),
"Trainer Parties": ItemData(None, ItemClassification.filler, [])
}
+10 -9
View File
@@ -10,9 +10,9 @@ def level_scaling(multiworld):
while locations:
sphere = set()
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
if (multiworld.level_scaling[world.player] != "by_spheres_and_distance"
and (multiworld.level_scaling[world.player] != "auto" or multiworld.door_shuffle[world.player]
in ("off", "simple"))):
if (world.options.level_scaling != "by_spheres_and_distance"
and (world.options.level_scaling != "auto"
or world.options.door_shuffle in ("off", "simple"))):
continue
regions = {multiworld.get_region("Menu", world.player)}
checked_regions = set()
@@ -41,7 +41,8 @@ def level_scaling(multiworld):
# reach them earlier. We treat them both as reachable right away for this purpose
return True
if (location.name == "Route 25 - Item" and state.can_reach("Route 25", "Region", location.player)
and multiworld.blind_trainers[location.player].value < 100):
and multiworld.worlds[location.player].options.blind_trainers.value < 100
and "Route 25 - Jr. Trainer M" not in multiworld.regions.location_cache[location.player]):
# Assume they will take their one chance to get the trainer to walk out of the way to reach
# the item behind them
return True
@@ -95,9 +96,9 @@ def level_scaling(multiworld):
if (location.item.game == "Pokemon Red and Blue" and (location.item.name.startswith("Missable ") or
location.item.name.startswith("Static ")) and location.name !=
"Pokemon Tower 6F - Restless Soul"):
# Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic static Pokemon
# are not considered for moves or evolutions, as you could release them and potentially soft lock
# the game. However, for level scaling purposes, we will treat them as not missable or static.
# Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic, and static
# Pokemon are not considered for moves or evolutions, as you could release them and potentially soft
# lock the game. However, for level scaling purposes, we will treat them as not missable or static.
# We would not want someone playing a minimal accessibility Dexsanity game to get what would be
# technically an "out of logic" Mansion Key from selecting Bulbasaur at the beginning of the game
# and end up in the Mansion early and encountering level 67 Pokémon
@@ -106,7 +107,7 @@ def level_scaling(multiworld):
else:
state.collect(location.item, True, location)
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
if multiworld.level_scaling[world.player] == "off":
if world.options.level_scaling == "off":
continue
level_list_copy = level_list.copy()
for sphere in spheres:
@@ -136,4 +137,4 @@ def level_scaling(multiworld):
else:
sphere_objects[object].level = level_list_copy.pop(0)
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
world.finished_level_scaling.set()
world.finished_level_scaling.set()
+27 -23
View File
@@ -5,46 +5,48 @@ from . import poke_data
loc_id_start = 172000000
def trainersanity(multiworld, player):
return multiworld.trainersanity[player]
def dexsanity(multiworld, player):
include = multiworld.worlds[player].dexsanity_table.pop(0)
multiworld.worlds[player].dexsanity_table.append(include)
def trainersanity(world, player):
include = world.trainersanity_table.pop(0)
world.trainersanity_table.append(include)
return include
def hidden_items(multiworld, player):
return multiworld.randomize_hidden_items[player]
def dexsanity(world, player):
include = world.dexsanity_table.pop(0)
world.dexsanity_table.append(include)
return include
def hidden_moon_stones(multiworld, player):
return multiworld.randomize_hidden_items[player] or multiworld.stonesanity[player]
def hidden_items(world, player):
return world.options.randomize_hidden_items
def tea(multiworld, player):
return multiworld.tea[player]
def hidden_moon_stones(world, player):
return world.options.randomize_hidden_items or world.options.stonesanity
def extra_key_items(multiworld, player):
return multiworld.extra_key_items[player]
def tea(world, player):
return world.options.tea
def always_on(multiworld, player):
def extra_key_items(world, player):
return world.options.extra_key_items
def always_on(world, player):
return True
def prizesanity(multiworld, player):
return multiworld.prizesanity[player]
def prizesanity(world, player):
return world.options.prizesanity
def split_card_key(multiworld, player):
return multiworld.split_card_key[player].value > 0
def split_card_key(world, player):
return world.options.split_card_key.value > 0
def not_stonesanity(multiworld, player):
return not multiworld.stonesanity[player]
def not_stonesanity(world, player):
return not world.options.stonesanity
class LocationData:
@@ -395,7 +397,7 @@ location_data = [
LocationData("Silph Co 5F", "Hidden Item Pot Plant", "Elixir", rom_addresses['Hidden_Item_Silph_Co_5F'], Hidden(18), inclusion=hidden_items),
LocationData("Silph Co 9F-SW", "Hidden Item Nurse Bed", "Max Potion", rom_addresses['Hidden_Item_Silph_Co_9F'], Hidden(19), inclusion=hidden_items),
LocationData("Saffron Copycat's House 2F", "Hidden Item Desk", "Nugget", rom_addresses['Hidden_Item_Copycats_House'], Hidden(20), inclusion=hidden_items),
LocationData("Cerulean Cave 1F-NW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items),
LocationData("Cerulean Cave 1F-SW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items),
LocationData("Cerulean Cave B1F-E", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22), inclusion=hidden_items),
LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23), inclusion=hidden_items),
LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24), inclusion=hidden_items),
@@ -786,6 +788,8 @@ location_data = [
LocationData("Celadon Game Corner", "", "Game Corner", event=True),
LocationData("Cinnabar Island", "", "Cinnabar Island", event=True),
LocationData("Cinnabar Lab", "", "Cinnabar Lab", event=True),
LocationData("Mt Moon B2F", "Mt Moon Fossils", "Mt Moon Fossils", event=True),
LocationData("Celadon Department Store 4F", "Buy Poke Doll", "Buy Poke Doll", event=True),
LocationData("Celadon Department Store 4F", "Buy Fire Stone", "Fire Stone", event=True, inclusion=not_stonesanity),
LocationData("Celadon Department Store 4F", "Buy Water Stone", "Water Stone", event=True, inclusion=not_stonesanity),
+38 -41
View File
@@ -1,49 +1,47 @@
from . import poke_data
def can_surf(state, player):
return (((state.has("HM03 Surf", player) and can_learn_hm(state, "Surf", player))
or state.has("Flippers", player)) and (state.has("Soul Badge", player) or
state.has(state.multiworld.worlds[player].extra_badges.get("Surf"), player)
or state.multiworld.badges_needed_for_hm_moves[player].value == 0))
def can_surf(state, world, player):
return (((state.has("HM03 Surf", player) and can_learn_hm(state, world, "Surf", player))) and (state.has("Soul Badge", player) or
state.has(world.extra_badges.get("Surf"), player)
or world.options.badges_needed_for_hm_moves.value == 0))
def can_cut(state, player):
return ((state.has("HM01 Cut", player) and can_learn_hm(state, "Cut", player) or state.has("Master Sword", player))
and (state.has("Cascade Badge", player) or
state.has(state.multiworld.worlds[player].extra_badges.get("Cut"), player) or
state.multiworld.badges_needed_for_hm_moves[player].value == 0))
def can_cut(state, world, player):
return ((state.has("HM01 Cut", player) and can_learn_hm(state, world, "Cut", player))
and (state.has("Cascade Badge", player) or state.has(world.extra_badges.get("Cut"), player) or
world.options.badges_needed_for_hm_moves.value == 0))
def can_fly(state, player):
return (((state.has("HM02 Fly", player) and can_learn_hm(state, "Fly", player)) or state.has("Flute", player)) and
(state.has("Thunder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Fly"), player)
or state.multiworld.badges_needed_for_hm_moves[player].value == 0))
def can_fly(state, world, player):
return (((state.has("HM02 Fly", player) and can_learn_hm(state, world, "Fly", player)) or state.has("Flute", player)) and
(state.has("Thunder Badge", player) or state.has(world.extra_badges.get("Fly"), player)
or world.options.badges_needed_for_hm_moves.value == 0))
def can_strength(state, player):
return ((state.has("HM04 Strength", player) and can_learn_hm(state, "Strength", player)) or
def can_strength(state, world, player):
return ((state.has("HM04 Strength", player) and can_learn_hm(state, world, "Strength", player)) or
state.has("Titan's Mitt", player)) and (state.has("Rainbow Badge", player) or
state.has(state.multiworld.worlds[player].extra_badges.get("Strength"), player)
or state.multiworld.badges_needed_for_hm_moves[player].value == 0)
state.has(world.extra_badges.get("Strength"), player)
or world.options.badges_needed_for_hm_moves.value == 0)
def can_flash(state, player):
return (((state.has("HM05 Flash", player) and can_learn_hm(state, "Flash", player)) or state.has("Lamp", player))
and (state.has("Boulder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Flash"),
player) or state.multiworld.badges_needed_for_hm_moves[player].value == 0))
def can_flash(state, world, player):
return (((state.has("HM05 Flash", player) and can_learn_hm(state, world, "Flash", player)) or state.has("Lamp", player))
and (state.has("Boulder Badge", player) or state.has(world.extra_badges.get("Flash"),
player) or world.options.badges_needed_for_hm_moves.value == 0))
def can_learn_hm(state, move, player):
for pokemon, data in state.multiworld.worlds[player].local_poke_data.items():
def can_learn_hm(state, world, move, player):
for pokemon, data in world.local_poke_data.items():
if state.has(pokemon, player) and data["tms"][6] & 1 << (["Cut", "Fly", "Surf", "Strength",
"Flash"].index(move) + 2):
return True
return False
def can_get_hidden_items(state, player):
return state.has("Item Finder", player) or not state.multiworld.require_item_finder[player].value
def can_get_hidden_items(state, world, player):
return state.has("Item Finder", player) or not world.options.require_item_finder.value
def has_key_items(state, count, player):
@@ -53,13 +51,14 @@ def has_key_items(state, count, player):
"Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F",
"Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F",
"Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone",
"Leaf Stone", "Moon Stone"] if state.has(item, player)])
"Leaf Stone", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil",
"Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if state.has(item, player)])
+ min(state.count("Progressive Card Key", player), 10))
return key_items >= count
def can_pass_guards(state, player):
if state.multiworld.tea[player]:
def can_pass_guards(state, world, player):
if world.options.tea:
return state.has("Tea", player)
else:
return state.has("Vending Machine Drinks", player)
@@ -70,8 +69,8 @@ def has_badges(state, count, player):
"Soul Badge", "Volcano Badge", "Earth Badge"] if state.has(item, player)]) >= count
def oaks_aide(state, count, player):
return ((not state.multiworld.require_pokedex[player] or state.has("Pokedex", player))
def oaks_aide(state, world, count, player):
return ((not world.options.require_pokedex or state.has("Pokedex", player))
and has_pokemon(state, count, player))
@@ -85,9 +84,7 @@ def has_pokemon(state, count, player):
def fossil_checks(state, count, player):
return (state.can_reach('Mt Moon B2F', 'Region', player) and
state.can_reach('Cinnabar Lab Fossil Room', 'Region', player) and
state.can_reach('Cinnabar Island', 'Region', player) and len(
return (state.has_all(["Mt Moon Fossils", "Cinnabar Lab", "Cinnabar Island"], player) and len(
[item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if state.has(item, player)]) >= count)
@@ -96,19 +93,19 @@ def card_key(state, floor, player):
state.has("Progressive Card Key", player, floor - 1)
def rock_tunnel(state, player):
return can_flash(state, player) or not state.multiworld.dark_rock_tunnel_logic[player]
def rock_tunnel(state, world, player):
return can_flash(state, world, player) or not world.options.dark_rock_tunnel_logic
def route_3(state, player):
if state.multiworld.route_3_condition[player] == "defeat_brock":
def route(state, world, player):
if world.options.route_3_condition == "defeat_brock":
return state.has("Defeat Brock", player)
elif state.multiworld.route_3_condition[player] == "defeat_any_gym":
elif world.options.route_3_condition == "defeat_any_gym":
return state.has_any(["Defeat Brock", "Defeat Misty", "Defeat Lt. Surge", "Defeat Erika", "Defeat Koga",
"Defeat Blaine", "Defeat Sabrina", "Defeat Viridian Gym Giovanni"], player)
elif state.multiworld.route_3_condition[player] == "boulder_badge":
elif world.options.route_3_condition == "boulder_badge":
return state.has("Boulder Badge", player)
elif state.multiworld.route_3_condition[player] == "any_badge":
elif world.options.route_3_condition == "any_badge":
return state.has_any(["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
"Soul Badge", "Volcano Badge", "Earth Badge"], player)
# open
+136 -117
View File
@@ -1,4 +1,6 @@
from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility
from dataclasses import dataclass
from Options import (PerGameCommonOptions, Toggle, Choice, Range, NamedRange, FreeText, TextChoice, DeathLink,
ItemsAccessibility)
class GameVersion(Choice):
@@ -263,12 +265,18 @@ class PrizeSanity(Toggle):
default = 0
class TrainerSanity(Toggle):
"""Add a location check to every trainer in the game, which can be obtained by talking to a trainer after defeating
them. Does not affect gym leaders and some scripted event battles (including all Rival, Giovanni, and
Cinnabar Gym battles)."""
class TrainerSanity(NamedRange):
"""Add location checks to trainers, which can be obtained by talking to a trainer after defeating them. Does not
affect gym leaders and some scripted event battles. You may specify a number of trainers to have checks, and in
this case they will be randomly selected. There is no in-game indication as to which trainers have checks."""
display_name = "Trainersanity"
default = 0
range_start = 0
range_end = 317
special_range_names = {
"disabled": 0,
"full": 317
}
class RequirePokedex(Toggle):
@@ -286,19 +294,19 @@ class AllPokemonSeen(Toggle):
class DexSanity(NamedRange):
"""Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to
have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable
Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage
of all 151 Pokemon.
If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to
Professor Oak or evaluating the Pokedex via Oak's PC."""
"""Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify the exact number of Dexsanity
checks to add, and they will be distributed to Pokemon randomly.
If Accessibility is set to Full, Dexsanity checks for Pokemon that are not logically reachable will be removed,
so the number could be lower than you specified.
If Pokedex is required, the Dexsanity checks for Pokemon you acquired before acquiring the Pokedex can be found by
talking to Professor Oak or evaluating the Pokedex via Oak's PC."""
display_name = "Dexsanity"
default = 0
range_start = 0
range_end = 100
range_end = 151
special_range_names = {
"disabled": 0,
"full": 100
"full": 151
}
@@ -519,7 +527,8 @@ class TrainerLegendaries(Toggle):
class BlindTrainers(Range):
"""Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a
battle. If you move into and out of their line of sight without stopping, this chance will only trigger once."""
battle. If you move into and out of their line of sight without stopping, this chance will only trigger once.
Trainers which have Trainersanity location checks ignore the Blind Trainers setting."""
display_name = "Blind Trainers"
range_start = 0
range_end = 100
@@ -704,6 +713,15 @@ class RandomizeTypeChart(Choice):
default = 0
class TypeChartSeed(FreeText):
"""You can enter a number to use as a seed for the type chart. If you enter anything besides a number or "random",
it will be used as a type chart group name, and everyone using the same group name will get the same type chart,
made using the type chart options of one random player within the group. If a group name is used, the type matchup
information will not be made available for trackers."""
display_name = "Type Chart Seed"
default = "random"
class NormalMatchups(Range):
"""If 'randomize' is chosen for Randomize Type Chart, this will be the weight for neutral matchups.
No effect if 'chaos' is chosen"""
@@ -850,8 +868,8 @@ class BicycleGateSkips(Choice):
class RandomizePokemonPalettes(Choice):
"""Modify palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, Follow
Evolutions will randomize palettes but palettes will remain the same through evolutions (except Eeveelutions),
"""Modify Super Gameboy palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type,
Follow Evolutions will randomize palettes but they will remain the same through evolutions (except Eeveelutions),
Completely Random will randomize all Pokemons' palettes individually"""
display_name = "Randomize Pokemon Palettes"
option_vanilla = 0
@@ -860,104 +878,105 @@ class RandomizePokemonPalettes(Choice):
option_completely_random = 3
pokemon_rb_options = {
"accessibility": ItemsAccessibility,
"game_version": GameVersion,
"trainer_name": TrainerName,
"rival_name": RivalName,
#"goal": Goal,
"elite_four_badges_condition": EliteFourBadgesCondition,
"elite_four_key_items_condition": EliteFourKeyItemsCondition,
"elite_four_pokedex_condition": EliteFourPokedexCondition,
"victory_road_condition": VictoryRoadCondition,
"route_22_gate_condition": Route22GateCondition,
"viridian_gym_condition": ViridianGymCondition,
"cerulean_cave_badges_condition": CeruleanCaveBadgesCondition,
"cerulean_cave_key_items_condition": CeruleanCaveKeyItemsCondition,
"route_3_condition": Route3Condition,
"robbed_house_officer": RobbedHouseOfficer,
"second_fossil_check_condition": SecondFossilCheckCondition,
"fossil_check_item_types": FossilCheckItemTypes,
"exp_all": ExpAll,
"old_man": OldMan,
"badgesanity": BadgeSanity,
"badges_needed_for_hm_moves": BadgesNeededForHMMoves,
"key_items_only": KeyItemsOnly,
"tea": Tea,
"extra_key_items": ExtraKeyItems,
"split_card_key": SplitCardKey,
"all_elevators_locked": AllElevatorsLocked,
"extra_strength_boulders": ExtraStrengthBoulders,
"require_item_finder": RequireItemFinder,
"randomize_hidden_items": RandomizeHiddenItems,
"prizesanity": PrizeSanity,
"trainersanity": TrainerSanity,
"dexsanity": DexSanity,
"randomize_pokedex": RandomizePokedex,
"require_pokedex": RequirePokedex,
"all_pokemon_seen": AllPokemonSeen,
"oaks_aide_rt_2": OaksAidRt2,
"oaks_aide_rt_11": OaksAidRt11,
"oaks_aide_rt_15": OaksAidRt15,
"stonesanity": Stonesanity,
"door_shuffle": DoorShuffle,
"warp_tile_shuffle": WarpTileShuffle,
"randomize_rock_tunnel": RandomizeRockTunnel,
"dark_rock_tunnel_logic": DarkRockTunnelLogic,
"free_fly_location": FreeFlyLocation,
"town_map_fly_location": TownMapFlyLocation,
"blind_trainers": BlindTrainers,
"minimum_steps_between_encounters": MinimumStepsBetweenEncounters,
"level_scaling": LevelScaling,
"exp_modifier": ExpModifier,
"randomize_wild_pokemon": RandomizeWildPokemon,
"area_1_to_1_mapping": Area1To1Mapping,
"randomize_starter_pokemon": RandomizeStarterPokemon,
"randomize_static_pokemon": RandomizeStaticPokemon,
"randomize_legendary_pokemon": RandomizeLegendaryPokemon,
"catch_em_all": CatchEmAll,
"randomize_pokemon_stats": RandomizePokemonStats,
"randomize_pokemon_catch_rates": RandomizePokemonCatchRates,
"minimum_catch_rate": MinimumCatchRate,
"randomize_trainer_parties": RandomizeTrainerParties,
"trainer_legendaries": TrainerLegendaries,
"move_balancing": MoveBalancing,
"fix_combat_bugs": FixCombatBugs,
"randomize_pokemon_movesets": RandomizePokemonMovesets,
"confine_transform_to_ditto": ConfineTranstormToDitto,
"start_with_four_moves": StartWithFourMoves,
"same_type_attack_bonus": SameTypeAttackBonus,
"randomize_tm_moves": RandomizeTMMoves,
"tm_same_type_compatibility": TMSameTypeCompatibility,
"tm_normal_type_compatibility": TMNormalTypeCompatibility,
"tm_other_type_compatibility": TMOtherTypeCompatibility,
"hm_same_type_compatibility": HMSameTypeCompatibility,
"hm_normal_type_compatibility": HMNormalTypeCompatibility,
"hm_other_type_compatibility": HMOtherTypeCompatibility,
"inherit_tm_hm_compatibility": InheritTMHMCompatibility,
"randomize_move_types": RandomizeMoveTypes,
"randomize_pokemon_types": RandomizePokemonTypes,
"secondary_type_chance": SecondaryTypeChance,
"randomize_type_chart": RandomizeTypeChart,
"normal_matchups": NormalMatchups,
"super_effective_matchups": SuperEffectiveMatchups,
"not_very_effective_matchups": NotVeryEffectiveMatchups,
"immunity_matchups": ImmunityMatchups,
"safari_zone_normal_battles": SafariZoneNormalBattles,
"normalize_encounter_chances": NormalizeEncounterChances,
"reusable_tms": ReusableTMs,
"better_shops": BetterShops,
"master_ball_price": MasterBallPrice,
"starting_money": StartingMoney,
"lose_money_on_blackout": LoseMoneyOnBlackout,
"poke_doll_skip": PokeDollSkip,
"bicycle_gate_skips": BicycleGateSkips,
"trap_percentage": TrapPercentage,
"poison_trap_weight": PoisonTrapWeight,
"fire_trap_weight": FireTrapWeight,
"paralyze_trap_weight": ParalyzeTrapWeight,
"sleep_trap_weight": SleepTrapWeight,
"ice_trap_weight": IceTrapWeight,
"randomize_pokemon_palettes": RandomizePokemonPalettes,
"death_link": DeathLink
}
@dataclass
class PokemonRBOptions(PerGameCommonOptions):
accessibility: ItemsAccessibility
game_version: GameVersion
trainer_name: TrainerName
rival_name: RivalName
# goal: Goal
elite_four_badges_condition: EliteFourBadgesCondition
elite_four_key_items_condition: EliteFourKeyItemsCondition
elite_four_pokedex_condition: EliteFourPokedexCondition
victory_road_condition: VictoryRoadCondition
route_22_gate_condition: Route22GateCondition
viridian_gym_condition: ViridianGymCondition
cerulean_cave_badges_condition: CeruleanCaveBadgesCondition
cerulean_cave_key_items_condition: CeruleanCaveKeyItemsCondition
route_3_condition: Route3Condition
robbed_house_officer: RobbedHouseOfficer
second_fossil_check_condition: SecondFossilCheckCondition
fossil_check_item_types: FossilCheckItemTypes
exp_all: ExpAll
old_man: OldMan
badgesanity: BadgeSanity
badges_needed_for_hm_moves: BadgesNeededForHMMoves
key_items_only: KeyItemsOnly
tea: Tea
extra_key_items: ExtraKeyItems
split_card_key: SplitCardKey
all_elevators_locked: AllElevatorsLocked
extra_strength_boulders: ExtraStrengthBoulders
require_item_finder: RequireItemFinder
randomize_hidden_items: RandomizeHiddenItems
prizesanity: PrizeSanity
trainersanity: TrainerSanity
dexsanity: DexSanity
randomize_pokedex: RandomizePokedex
require_pokedex: RequirePokedex
all_pokemon_seen: AllPokemonSeen
oaks_aide_rt_2: OaksAidRt2
oaks_aide_rt_11: OaksAidRt11
oaks_aide_rt_15: OaksAidRt15
stonesanity: Stonesanity
door_shuffle: DoorShuffle
warp_tile_shuffle: WarpTileShuffle
randomize_rock_tunnel: RandomizeRockTunnel
dark_rock_tunnel_logic: DarkRockTunnelLogic
free_fly_location: FreeFlyLocation
town_map_fly_location: TownMapFlyLocation
blind_trainers: BlindTrainers
minimum_steps_between_encounters: MinimumStepsBetweenEncounters
level_scaling: LevelScaling
exp_modifier: ExpModifier
randomize_wild_pokemon: RandomizeWildPokemon
area_1_to_1_mapping: Area1To1Mapping
randomize_starter_pokemon: RandomizeStarterPokemon
randomize_static_pokemon: RandomizeStaticPokemon
randomize_legendary_pokemon: RandomizeLegendaryPokemon
catch_em_all: CatchEmAll
randomize_pokemon_stats: RandomizePokemonStats
randomize_pokemon_catch_rates: RandomizePokemonCatchRates
minimum_catch_rate: MinimumCatchRate
randomize_trainer_parties: RandomizeTrainerParties
trainer_legendaries: TrainerLegendaries
move_balancing: MoveBalancing
fix_combat_bugs: FixCombatBugs
randomize_pokemon_movesets: RandomizePokemonMovesets
confine_transform_to_ditto: ConfineTranstormToDitto
start_with_four_moves: StartWithFourMoves
same_type_attack_bonus: SameTypeAttackBonus
randomize_tm_moves: RandomizeTMMoves
tm_same_type_compatibility: TMSameTypeCompatibility
tm_normal_type_compatibility: TMNormalTypeCompatibility
tm_other_type_compatibility: TMOtherTypeCompatibility
hm_same_type_compatibility: HMSameTypeCompatibility
hm_normal_type_compatibility: HMNormalTypeCompatibility
hm_other_type_compatibility: HMOtherTypeCompatibility
inherit_tm_hm_compatibility: InheritTMHMCompatibility
randomize_move_types: RandomizeMoveTypes
randomize_pokemon_types: RandomizePokemonTypes
secondary_type_chance: SecondaryTypeChance
randomize_type_chart: RandomizeTypeChart
normal_matchups: NormalMatchups
super_effective_matchups: SuperEffectiveMatchups
not_very_effective_matchups: NotVeryEffectiveMatchups
immunity_matchups: ImmunityMatchups
type_chart_seed: TypeChartSeed
safari_zone_normal_battles: SafariZoneNormalBattles
normalize_encounter_chances: NormalizeEncounterChances
reusable_tms: ReusableTMs
better_shops: BetterShops
master_ball_price: MasterBallPrice
starting_money: StartingMoney
lose_money_on_blackout: LoseMoneyOnBlackout
poke_doll_skip: PokeDollSkip
bicycle_gate_skips: BicycleGateSkips
trap_percentage: TrapPercentage
poison_trap_weight: PoisonTrapWeight
fire_trap_weight: FireTrapWeight
paralyze_trap_weight: ParalyzeTrapWeight
sleep_trap_weight: SleepTrapWeight
ice_trap_weight: IceTrapWeight
randomize_pokemon_palettes: RandomizePokemonPalettes
death_link: DeathLink
+106 -109
View File
@@ -3,8 +3,8 @@ from . import poke_data, logic
from .rom_addresses import rom_addresses
def set_mon_palettes(self, random, data):
if self.multiworld.randomize_pokemon_palettes[self.player] == "vanilla":
def set_mon_palettes(world, random, data):
if world.options.randomize_pokemon_palettes == "vanilla":
return
pallet_map = {
"Poison": 0x0F,
@@ -25,9 +25,9 @@ def set_mon_palettes(self, random, data):
}
palettes = []
for mon in poke_data.pokemon_data:
if self.multiworld.randomize_pokemon_palettes[self.player] == "primary_type":
pallet = pallet_map[self.local_poke_data[mon]["type1"]]
elif (self.multiworld.randomize_pokemon_palettes[self.player] == "follow_evolutions" and mon in
if world.options.randomize_pokemon_palettes == "primary_type":
pallet = pallet_map[world.local_poke_data[mon]["type1"]]
elif (world.options.randomize_pokemon_palettes == "follow_evolutions" and mon in
poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"):
pallet = palettes[-1]
else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions)
@@ -93,40 +93,41 @@ def move_power(move_data):
return power
def process_move_data(self):
self.local_move_data = deepcopy(poke_data.moves)
def process_move_data(world):
world.local_move_data = deepcopy(poke_data.moves)
if self.multiworld.randomize_move_types[self.player]:
for move, data in self.local_move_data.items():
if world.options.randomize_move_types:
for move, data in world.local_move_data.items():
if move == "No Move":
continue
# The chance of randomized moves choosing a normal type move is high, so we want to retain having a higher
# rate of normal type moves
data["type"] = self.multiworld.random.choice(list(poke_data.type_ids) + (["Normal"] * 4))
data["type"] = world.random.choice(list(poke_data.type_ids) + (["Normal"] * 4))
if self.multiworld.move_balancing[self.player]:
self.local_move_data["Sing"]["accuracy"] = 30
self.local_move_data["Sleep Powder"]["accuracy"] = 40
self.local_move_data["Spore"]["accuracy"] = 50
self.local_move_data["Sonicboom"]["effect"] = 0
self.local_move_data["Sonicboom"]["power"] = 50
self.local_move_data["Dragon Rage"]["effect"] = 0
self.local_move_data["Dragon Rage"]["power"] = 80
self.local_move_data["Horn Drill"]["effect"] = 0
self.local_move_data["Horn Drill"]["power"] = 70
self.local_move_data["Horn Drill"]["accuracy"] = 90
self.local_move_data["Guillotine"]["effect"] = 0
self.local_move_data["Guillotine"]["power"] = 70
self.local_move_data["Guillotine"]["accuracy"] = 90
self.local_move_data["Fissure"]["effect"] = 0
self.local_move_data["Fissure"]["power"] = 70
self.local_move_data["Fissure"]["accuracy"] = 90
self.local_move_data["Blizzard"]["accuracy"] = 70
if self.multiworld.randomize_tm_moves[self.player]:
self.local_tms = self.multiworld.random.sample([move for move in poke_data.moves.keys() if move not in
["No Move"] + poke_data.hm_moves], 50)
if world.options.move_balancing:
world.local_move_data["Sing"]["accuracy"] = 30
world.local_move_data["Sleep Powder"]["accuracy"] = 40
world.local_move_data["Spore"]["accuracy"] = 50
world.local_move_data["Sonicboom"]["effect"] = 0
world.local_move_data["Sonicboom"]["power"] = 50
world.local_move_data["Dragon Rage"]["effect"] = 0
world.local_move_data["Dragon Rage"]["power"] = 80
world.local_move_data["Horn Drill"]["effect"] = 0
world.local_move_data["Horn Drill"]["power"] = 70
world.local_move_data["Horn Drill"]["accuracy"] = 90
world.local_move_data["Guillotine"]["effect"] = 0
world.local_move_data["Guillotine"]["power"] = 70
world.local_move_data["Guillotine"]["accuracy"] = 90
world.local_move_data["Fissure"]["effect"] = 0
world.local_move_data["Fissure"]["power"] = 70
world.local_move_data["Fissure"]["accuracy"] = 90
world.local_move_data["Blizzard"]["accuracy"] = 70
if world.options.randomize_tm_moves:
world.local_tms = world.random.sample([move for move in poke_data.moves.keys() if move not in
["No Move"] + poke_data.hm_moves], 50)
else:
self.local_tms = poke_data.tm_moves.copy()
world.local_tms = poke_data.tm_moves.copy()
def process_pokemon_data(self):
@@ -138,12 +139,12 @@ def process_pokemon_data(self):
compat_hms = set()
for mon, mon_data in local_poke_data.items():
if self.multiworld.randomize_pokemon_stats[self.player] == "shuffle":
if self.options.randomize_pokemon_stats == "shuffle":
stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]]
if mon in poke_data.evolves_from:
stat_shuffle_map = local_poke_data[poke_data.evolves_from[mon]]["stat_shuffle_map"]
else:
stat_shuffle_map = self.multiworld.random.sample(range(0, 5), 5)
stat_shuffle_map = self.random.sample(range(0, 5), 5)
mon_data["stat_shuffle_map"] = stat_shuffle_map
mon_data["hp"] = stats[stat_shuffle_map[0]]
@@ -151,7 +152,7 @@ def process_pokemon_data(self):
mon_data["def"] = stats[stat_shuffle_map[2]]
mon_data["spd"] = stats[stat_shuffle_map[3]]
mon_data["spc"] = stats[stat_shuffle_map[4]]
elif self.multiworld.randomize_pokemon_stats[self.player] == "randomize":
elif self.options.randomize_pokemon_stats == "randomize":
first_run = True
while (mon_data["hp"] > 255 or mon_data["atk"] > 255 or mon_data["def"] > 255 or mon_data["spd"] > 255
or mon_data["spc"] > 255 or first_run):
@@ -168,9 +169,9 @@ def process_pokemon_data(self):
mon_data[stat] = 10
total_stats -= 10
assert total_stats >= 0, f"Error distributing stats for {mon} for player {self.player}"
dist = [self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100,
self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100,
self.multiworld.random.randint(1, 101) / 100]
dist = [self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100,
self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100,
self.random.randint(1, 101) / 100]
total_dist = sum(dist)
mon_data["hp"] += int(round(dist[0] / total_dist * total_stats))
@@ -178,30 +179,30 @@ def process_pokemon_data(self):
mon_data["def"] += int(round(dist[2] / total_dist * total_stats))
mon_data["spd"] += int(round(dist[3] / total_dist * total_stats))
mon_data["spc"] += int(round(dist[4] / total_dist * total_stats))
if self.multiworld.randomize_pokemon_types[self.player]:
if self.multiworld.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from:
if self.options.randomize_pokemon_types:
if self.options.randomize_pokemon_types.value == 1 and mon in poke_data.evolves_from:
type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"]
type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"]
if type1 == type2:
if self.multiworld.secondary_type_chance[self.player].value == -1:
if self.options.secondary_type_chance.value == -1:
if mon_data["type1"] != mon_data["type2"]:
while type2 == type1:
type2 = self.multiworld.random.choice(list(poke_data.type_names.values()))
elif self.multiworld.random.randint(1, 100) <= self.multiworld.secondary_type_chance[self.player].value:
type2 = self.multiworld.random.choice(list(poke_data.type_names.values()))
type2 = self.random.choice(list(poke_data.type_names.values()))
elif self.random.randint(1, 100) <= self.options.secondary_type_chance.value:
type2 = self.random.choice(list(poke_data.type_names.values()))
else:
type1 = self.multiworld.random.choice(list(poke_data.type_names.values()))
type1 = self.random.choice(list(poke_data.type_names.values()))
type2 = type1
if ((self.multiworld.secondary_type_chance[self.player].value == -1 and mon_data["type1"]
!= mon_data["type2"]) or self.multiworld.random.randint(1, 100)
<= self.multiworld.secondary_type_chance[self.player].value):
if ((self.options.secondary_type_chance.value == -1 and mon_data["type1"]
!= mon_data["type2"]) or self.random.randint(1, 100)
<= self.options.secondary_type_chance.value):
while type2 == type1:
type2 = self.multiworld.random.choice(list(poke_data.type_names.values()))
type2 = self.random.choice(list(poke_data.type_names.values()))
mon_data["type1"] = type1
mon_data["type2"] = type2
if self.multiworld.randomize_pokemon_movesets[self.player]:
if self.multiworld.randomize_pokemon_movesets[self.player] == "prefer_types":
if self.options.randomize_pokemon_movesets:
if self.options.randomize_pokemon_movesets == "prefer_types":
if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal":
chances = [[75, "Normal"]]
elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal":
@@ -219,9 +220,9 @@ def process_pokemon_data(self):
moves = list(poke_data.moves.keys())
for move in ["No Move"] + poke_data.hm_moves:
moves.remove(move)
if self.multiworld.confine_transform_to_ditto[self.player]:
if self.options.confine_transform_to_ditto:
moves.remove("Transform")
if self.multiworld.start_with_four_moves[self.player]:
if self.options.start_with_four_moves:
num_moves = 4
else:
num_moves = len([i for i in [mon_data["start move 1"], mon_data["start move 2"],
@@ -231,12 +232,12 @@ def process_pokemon_data(self):
non_power_moves = []
learnsets[mon] = []
for i in range(num_moves):
if i == 0 and mon == "Ditto" and self.multiworld.confine_transform_to_ditto[self.player]:
if i == 0 and mon == "Ditto" and self.options.confine_transform_to_ditto:
move = "Transform"
else:
move = get_move(self.local_move_data, moves, chances, self.multiworld.random)
while move == "Transform" and self.multiworld.confine_transform_to_ditto[self.player]:
move = get_move(self.local_move_data, moves, chances, self.multiworld.random)
move = get_move(self.local_move_data, moves, chances, self.random)
while move == "Transform" and self.options.confine_transform_to_ditto:
move = get_move(self.local_move_data, moves, chances, self.random)
if self.local_move_data[move]["power"] < 5:
non_power_moves.append(move)
else:
@@ -244,59 +245,58 @@ def process_pokemon_data(self):
learnsets[mon].sort(key=lambda move: move_power(self.local_move_data[move]))
if learnsets[mon]:
for move in non_power_moves:
learnsets[mon].insert(self.multiworld.random.randint(1, len(learnsets[mon])), move)
learnsets[mon].insert(self.random.randint(1, len(learnsets[mon])), move)
else:
learnsets[mon] = non_power_moves
for i in range(1, 5):
if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[self.player]:
if mon_data[f"start move {i}"] != "No Move" or self.options.start_with_four_moves:
mon_data[f"start move {i}"] = learnsets[mon].pop(0)
if self.multiworld.randomize_pokemon_catch_rates[self.player]:
mon_data["catch rate"] = self.multiworld.random.randint(self.multiworld.minimum_catch_rate[self.player],
255)
if self.options.randomize_pokemon_catch_rates:
mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate, 255)
else:
mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"])
mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"])
def roll_tm_compat(roll_move):
if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]:
if roll_move in poke_data.hm_moves:
if self.multiworld.hm_same_type_compatibility[self.player].value == -1:
if self.options.hm_same_type_compatibility.value == -1:
return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8)
r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_same_type_compatibility[self.player].value
r = self.random.randint(1, 100) <= self.options.hm_same_type_compatibility.value
if r and mon not in poke_data.legendary_pokemon:
compat_hms.add(roll_move)
return r
else:
if self.multiworld.tm_same_type_compatibility[self.player].value == -1:
if self.options.tm_same_type_compatibility.value == -1:
return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8)
return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_same_type_compatibility[self.player].value
return self.random.randint(1, 100) <= self.options.tm_same_type_compatibility.value
elif self.local_move_data[roll_move]["type"] == "Normal" and "Normal" not in [mon_data["type1"], mon_data["type2"]]:
if roll_move in poke_data.hm_moves:
if self.multiworld.hm_normal_type_compatibility[self.player].value == -1:
if self.options.hm_normal_type_compatibility.value == -1:
return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8)
r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_normal_type_compatibility[self.player].value
r = self.random.randint(1, 100) <= self.options.hm_normal_type_compatibility.value
if r and mon not in poke_data.legendary_pokemon:
compat_hms.add(roll_move)
return r
else:
if self.multiworld.tm_normal_type_compatibility[self.player].value == -1:
if self.options.tm_normal_type_compatibility.value == -1:
return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8)
return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_normal_type_compatibility[self.player].value
return self.random.randint(1, 100) <= self.options.tm_normal_type_compatibility.value
else:
if roll_move in poke_data.hm_moves:
if self.multiworld.hm_other_type_compatibility[self.player].value == -1:
if self.options.hm_other_type_compatibility.value == -1:
return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8)
r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_other_type_compatibility[self.player].value
r = self.random.randint(1, 100) <= self.options.hm_other_type_compatibility.value
if r and mon not in poke_data.legendary_pokemon:
compat_hms.add(roll_move)
return r
else:
if self.multiworld.tm_other_type_compatibility[self.player].value == -1:
if self.options.tm_other_type_compatibility.value == -1:
return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8)
return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_other_type_compatibility[self.player].value
return self.random.randint(1, 100) <= self.options.tm_other_type_compatibility.value
for flag, tm_move in enumerate(tms_hms):
if mon in poke_data.evolves_from and self.multiworld.inherit_tm_hm_compatibility[self.player]:
if mon in poke_data.evolves_from and self.options.inherit_tm_hm_compatibility:
if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8):
# always inherit learnable tms/hms
@@ -310,7 +310,7 @@ def process_pokemon_data(self):
# so this gets full chance roll
bit = roll_tm_compat(tm_move)
# otherwise 50% reduced chance to add compatibility over pre-evolved form
elif self.multiworld.random.randint(1, 100) > 50 and roll_tm_compat(tm_move):
elif self.random.randint(1, 100) > 50 and roll_tm_compat(tm_move):
bit = 1
else:
bit = 0
@@ -322,15 +322,13 @@ def process_pokemon_data(self):
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
hm_verify = ["Surf", "Strength"]
if self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")):
if self.options.accessibility != "minimal" or ((not
self.options.badgesanity) and max(self.options.elite_four_badges_condition,
self.options.route_22_gate_condition, self.options.victory_road_condition)
> 7) or (self.options.door_shuffle not in ("off", "simple")):
hm_verify += ["Cut"]
if self.multiworld.accessibility[self.player] != "minimal" or (not
self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or
self.multiworld.extra_key_items[self.player])
or self.multiworld.door_shuffle[self.player]):
if (self.options.accessibility != "minimal" or (not self.options.dark_rock_tunnel_logic) and
((self.options.trainersanity or self.options.extra_key_items) or self.options.door_shuffle)):
hm_verify += ["Flash"]
# Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable
# regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for
@@ -339,8 +337,7 @@ def process_pokemon_data(self):
for hm_move in hm_verify:
if hm_move not in compat_hms:
mon = self.multiworld.random.choice([mon for mon in poke_data.pokemon_data if mon not in
poke_data.legendary_pokemon])
mon = self.random.choice([mon for mon in poke_data.pokemon_data if mon not in poke_data.legendary_pokemon])
flag = tms_hms.index(hm_move)
local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
@@ -352,7 +349,7 @@ def verify_hm_moves(multiworld, world, player):
def intervene(move, test_state):
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit]
if multiworld.randomize_wild_pokemon[player] and viable_mons:
if world.options.randomize_wild_pokemon and viable_mons:
accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if
loc.type == "Wild Encounter"]
@@ -364,7 +361,7 @@ def verify_hm_moves(multiworld, world, player):
placed_mons = [slot.item.name for slot in accessible_slots]
if multiworld.area_1_to_1_mapping[player]:
if world.options.area_1_to_1_mapping:
placed_mons.sort(key=lambda i: number_of_zones(i))
else:
# this sort method doesn't work if you reference the same list being sorted in the lambda
@@ -372,10 +369,10 @@ def verify_hm_moves(multiworld, world, player):
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
replace_mon = multiworld.random.choice(viable_mons)
replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
replace_mon = world.random.choice(viable_mons)
replace_slot = world.random.choice([slot for slot in accessible_slots if slot.item.name
== placed_mon])
if multiworld.area_1_to_1_mapping[player]:
if world.options.area_1_to_1_mapping:
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
== placed_mon]
@@ -387,7 +384,7 @@ def verify_hm_moves(multiworld, world, player):
tms_hms = world.local_tms + poke_data.hm_moves
flag = tms_hms.index(move)
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)]
multiworld.random.shuffle(mon_list)
world.random.shuffle(mon_list)
mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in
[world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]])
for mon in mon_list:
@@ -399,31 +396,31 @@ def verify_hm_moves(multiworld, world, player):
while True:
intervene_move = None
test_state = multiworld.get_all_state(False)
if not logic.can_learn_hm(test_state, "Surf", player):
if not logic.can_learn_hm(test_state, world, "Surf", player):
intervene_move = "Surf"
elif not logic.can_learn_hm(test_state, "Strength", player):
elif not logic.can_learn_hm(test_state, world, "Strength", player):
intervene_move = "Strength"
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
# as you will require cut to access celadon gyn
elif ((not logic.can_learn_hm(test_state, "Cut", player)) and
(multiworld.accessibility[player] != "minimal" or ((not
multiworld.badgesanity[player]) and max(
multiworld.elite_four_badges_condition[player],
multiworld.route_22_gate_condition[player],
multiworld.victory_road_condition[player])
> 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))):
elif ((not logic.can_learn_hm(test_state, world, "Cut", player)) and
(world.options.accessibility != "minimal" or ((not
world.options.badgesanity) and max(
world.options.elite_four_badges_condition,
world.options.route_22_gate_condition,
world.options.victory_road_condition)
> 7) or (world.options.door_shuffle not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", player))
and multiworld.dark_rock_tunnel_logic[player]
and (multiworld.accessibility[player] != "minimal"
or multiworld.door_shuffle[player])):
elif ((not logic.can_learn_hm(test_state, world, "Flash", player))
and world.options.dark_rock_tunnel_logic
and (world.options.accessibility != "minimal"
or world.options.door_shuffle)):
intervene_move = "Flash"
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
# considered in door shuffle.
elif ((not logic.can_learn_hm(test_state, "Fly", player))
and multiworld.door_shuffle[player] not in
elif ((not logic.can_learn_hm(test_state, world, "Fly", player))
and world.options.door_shuffle not in
("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
intervene_move = "Fly"
if intervene_move:
@@ -432,4 +429,4 @@ def verify_hm_moves(multiworld, world, player):
intervene(intervene_move, test_state)
last_intervene = intervene_move
else:
break
break
File diff suppressed because it is too large Load Diff
+210 -166
View File
@@ -13,22 +13,22 @@ from .regions import PokemonRBWarp, map_ids, town_map_coords
from . import poke_data
def write_quizzes(self, data, random):
def write_quizzes(world, data, random):
def get_quiz(q, a):
if q == 0:
r = random.randint(0, 3)
if r == 0:
mon = self.trade_mons["Trade_Dux"]
mon = world.trade_mons["Trade_Dux"]
text = "A woman in<LINE>Vermilion City<CONT>"
elif r == 1:
mon = self.trade_mons["Trade_Lola"]
mon = world.trade_mons["Trade_Lola"]
text = "A man in<LINE>Cerulean City<CONT>"
elif r == 2:
mon = self.trade_mons["Trade_Marcel"]
mon = world.trade_mons["Trade_Marcel"]
text = "Someone on Route 2<LINE>"
elif r == 3:
mon = self.trade_mons["Trade_Spot"]
mon = world.trade_mons["Trade_Spot"]
text = "Someone on Route 5<LINE>"
if not a:
answers.append(0)
@@ -38,21 +38,30 @@ def write_quizzes(self, data, random):
return encode_text(f"{text}was looking for<CONT>{mon}?<DONE>")
elif q == 1:
for location in self.multiworld.get_filled_locations():
if location.item.name == "Secret Key" and location.item.player == self.player:
for location in world.multiworld.get_filled_locations():
if location.item.name == "Secret Key" and location.item.player == world.player:
break
player_name = self.multiworld.player_name[location.player]
player_name = world.multiworld.player_name[location.player]
if not a:
if len(self.multiworld.player_name) > 1:
if len(world.multiworld.player_name) > 1:
old_name = player_name
while old_name == player_name:
player_name = random.choice(list(self.multiworld.player_name.values()))
player_name = random.choice(list(world.multiworld.player_name.values()))
else:
return encode_text("You're playing<LINE>in a multiworld<CONT>with other<CONT>players?<DONE>")
if player_name == self.multiworld.player_name[self.player]:
player_name = "yourself"
player_name = encode_text(player_name, force=True, safety=True)
return encode_text(f"The Secret Key was<LINE>found by<CONT>") + player_name + encode_text("<DONE>")
if world.multiworld.get_entrance(
"Cinnabar Island-G to Cinnabar Gym", world.player).connected_region.name == "Cinnabar Gym":
if player_name == world.multiworld.player_name[world.player]:
player_name = "yourself"
player_name = encode_text(player_name, force=True, safety=True)
return encode_text(f"The Secret Key was<LINE>found by<CONT>") + player_name + encode_text("?<DONE>")
else:
# Might not have found it yet
if player_name == world.multiworld.player_name[world.player]:
return encode_text(f"The Secret Key was<LINE>placed in<CONT>your own world?<DONE>")
player_name = encode_text(player_name, force=True, safety=True)
return (encode_text(f"The Secret Key was<LINE>placed in<CONT>") + player_name
+ encode_text("'s<CONT>world?<DONE>"))
elif q == 2:
if a:
return encode_text(f"#mon is<LINE>pronounced<CONT>Po-kay-mon?<DONE>")
@@ -62,8 +71,8 @@ def write_quizzes(self, data, random):
else:
return encode_text(f"#mon is<LINE>pronounced<CONT>Po-kuh-mon?<DONE>")
elif q == 3:
starters = [" ".join(self.multiworld.get_location(
f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
starters = [" ".join(world.multiworld.get_location(
f"Oak's Lab - Starter {i}", world.player).item.name.split(" ")[1:]) for i in range(1, 4)]
mon = random.choice(starters)
nots = random.choice(range(8, 16, 2))
if random.randint(0, 1):
@@ -82,10 +91,10 @@ def write_quizzes(self, data, random):
return encode_text(text)
elif q == 4:
if a:
tm_text = self.local_tms[27]
tm_text = world.local_tms[27]
else:
if self.multiworld.randomize_tm_moves[self.player]:
wrong_tms = self.local_tms.copy()
if world.options.randomize_tm_moves:
wrong_tms = world.local_tms.copy()
wrong_tms.pop(27)
tm_text = random.choice(wrong_tms)
else:
@@ -102,12 +111,36 @@ def write_quizzes(self, data, random):
i = random.randint(0, random.choice([9, 99]))
return encode_text(f"POLIWAG evolves {i}<LINE>times?<DONE>")
elif q == 7:
entity = "Motor Carrier"
if not a:
entity = random.choice(["Driver", "Shipper"])
return encode_text("Title 49 of the<LINE>U.S. Code of<CONT>Federal<CONT>Regulations part<CONT>397.67 states"
f"<CONT>that the<CONT>{entity}<CONT>is responsible<CONT>for planning<CONT>routes when"
"<CONT>hazardous<CONT>materials are<CONT>transported?<DONE>")
q2 = random.randint(0, 2)
if q2 == 0:
entity = "Motor Carrier"
if not a:
entity = random.choice(["Driver", "Shipper"])
return encode_text("Title 49 of the<LINE>U.S. Code of<CONT>Federal<CONT>Regulations part<CONT>397.67 "
f"states<CONT>that the<CONT>{entity}<CONT>is responsible<CONT>for planning<CONT>"
"routes when<CONT>hazardous<CONT>materials are<CONT>transported?<DONE>")
elif q2 == 1:
if a:
state = random.choice(
['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut',
'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas',
'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Jersey', 'New Mexico',
'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'])
else:
state = "New Hampshire"
return encode_text(
f"As of 2024,<LINE>{state}<CONT>has a law<CONT>requiring all<CONT>front seat vehicle<CONT>occupants to use<CONT>seatbelts?<DONE>")
elif q2 == 2:
if a:
country = random.choice(["The United States", "Mexico", "Canada", "Germany", "France", "China",
"Russia", "Spain", "Brazil", "Ukraine", "Saudi Arabia", "Egypt"])
else:
country = random.choice(["The U.K.", "Pakistan", "India", "Japan", "Australia",
"New Zealand", "Thailand"])
return encode_text(f"As of 2020,<LINE>drivers in<CONT>{country}<CONT>drive on the<CONT>right side of<CONT>the road?<DONE>")
elif q == 8:
mon = random.choice(list(poke_data.evolution_levels.keys()))
level = poke_data.evolution_levels[mon]
@@ -115,17 +148,17 @@ def write_quizzes(self, data, random):
level += random.choice(range(1, 6)) * random.choice((-1, 1))
return encode_text(f"{mon} evolves<LINE>at level {level}?<DONE>")
elif q == 9:
move = random.choice(list(self.local_move_data.keys()))
actual_type = self.local_move_data[move]["type"]
move = random.choice(list(world.local_move_data.keys()))
actual_type = world.local_move_data[move]["type"]
question_type = actual_type
while question_type == actual_type and not a:
question_type = random.choice(list(poke_data.type_ids.keys()))
return encode_text(f"{move} is<LINE>{question_type} type?<DONE>")
elif q == 10:
mon = random.choice(list(poke_data.pokemon_data.keys()))
actual_type = self.local_poke_data[mon][random.choice(("type1", "type2"))]
actual_type = world.local_poke_data[mon][random.choice(("type1", "type2"))]
question_type = actual_type
while question_type in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]] and not a:
while question_type in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]] and not a:
question_type = random.choice(list(poke_data.type_ids.keys()))
return encode_text(f"{mon} is<LINE>{question_type} type?<DONE>")
elif q == 11:
@@ -147,8 +180,8 @@ def write_quizzes(self, data, random):
return encode_text(f"{equation}<LINE>= {question_result}?<DONE>")
elif q == 12:
route = random.choice((12, 16))
actual_mon = self.multiworld.get_location(f"Route {route} - Sleeping Pokemon",
self.player).item.name.split("Static ")[1]
actual_mon = world.multiworld.get_location(f"Route {route} - Sleeping Pokemon",
world.player).item.name.split("Static ")[1]
question_mon = actual_mon
while question_mon == actual_mon and not a:
question_mon = random.choice(list(poke_data.pokemon_data.keys()))
@@ -157,7 +190,7 @@ def write_quizzes(self, data, random):
type1 = random.choice(list(poke_data.type_ids.keys()))
type2 = random.choice(list(poke_data.type_ids.keys()))
eff_msgs = ["super effective<CONT>", "no ", "not very<CONT>effective<CONT>", "normal "]
for matchup in self.type_chart:
for matchup in world.type_chart:
if matchup[0] == type1 and matchup[1] == type2:
if matchup[2] > 10:
eff = eff_msgs[0]
@@ -175,15 +208,25 @@ def write_quizzes(self, data, random):
eff = random.choice(eff_msgs)
return encode_text(f"{type1} deals<LINE>{eff}damage to<CONT>{type2} type?<DONE>")
elif q == 14:
fossil_level = self.multiworld.get_location("Fossil Level - Trainer Parties",
self.player).party_data[0]['level']
fossil_level = world.multiworld.get_location("Fossil Level - Trainer Parties",
world.player).party_data[0]['level']
if not a:
fossil_level += random.choice((-5, 5))
return encode_text(f"Fossil #MON<LINE>revive at level<CONT>{fossil_level}?<DONE>")
elif q == 15:
if a:
fodmap = random.choice(["garlic", "onion", "milk", "watermelon", "cherries", "wheat", "barley",
"pistachios", "cashews", "kidney beans", "apples", "honey"])
else:
fodmap = random.choice(["carrots", "potatoes", "oranges", "pineapple", "blueberries", "parmesan",
"eggs", "beef", "chicken", "oat", "rice", "maple syrup", "peanuts"])
are_is = "are" if fodmap[-1] == "s" else "is"
return encode_text(f"According to<LINE>Monash Uni.,<CONT>{fodmap} {are_is}<CONT>considered high<CONT>in FODMAPs?<DONE>")
answers = [random.randint(0, 1) for _ in range(6)]
questions = random.sample((range(0, 15)), 6)
questions = random.sample((range(0, 16)), 6)
question_texts = []
for i, question in enumerate(questions):
question_texts.append(get_quiz(question, answers[i]))
@@ -193,9 +236,9 @@ def write_quizzes(self, data, random):
write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"])
def generate_output(self, output_directory: str):
random = self.multiworld.per_slot_randoms[self.player]
game_version = self.multiworld.game_version[self.player].current_key
def generate_output(world, output_directory: str):
random = world.random
game_version = world.options.game_version.current_key
data = bytes(get_base_rom_bytes(game_version))
base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4')
@@ -205,8 +248,8 @@ def generate_output(self, output_directory: str):
basemd5 = hashlib.md5()
basemd5.update(data)
pallet_connections = {entrance: self.multiworld.get_entrance(f"Pallet Town to {entrance}",
self.player).connected_region.name for
pallet_connections = {entrance: world.multiworld.get_entrance(f"Pallet Town to {entrance}",
world.player).connected_region.name for
entrance in ["Player's House 1F", "Oak's Lab",
"Rival's House"]}
paths = None
@@ -222,11 +265,11 @@ def generate_output(self, output_directory: str):
elif pallet_connections["Oak's Lab"] == "Player's House 1F":
write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"])
for region in self.multiworld.get_regions(self.player):
for region in world.multiworld.get_regions(world.player):
for entrance in region.exits:
if isinstance(entrance, PokemonRBWarp):
self.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance",
self.player)
world.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance",
world.player)
warp_ids = (entrance.warp_id,) if isinstance(entrance.warp_id, int) else entrance.warp_id
warp_to_ids = (entrance.target,) if isinstance(entrance.target, int) else entrance.target
for i, warp_id in enumerate(warp_ids):
@@ -241,32 +284,32 @@ def generate_output(self, output_directory: str):
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
data[address + 1] = map_ids[connected_map_name]
if self.multiworld.door_shuffle[self.player] == "simple":
if world.options.door_shuffle == "simple":
for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values():
destination = self.multiworld.get_entrance(entrance, self.player).connected_region.name
destination = world.multiworld.get_entrance(entrance, world.player).connected_region.name
(_, x, y, _, _, map_order_entry) = town_map_coords[destination]
for map_coord_entry in map_coords_entries:
data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x
data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name]
if not self.multiworld.key_items_only[self.player]:
if not world.options.key_items_only:
for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
"Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
"Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM",
"Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")):
item_name = self.multiworld.get_location(gym_leader, self.player).item.name
item_name = world.multiworld.get_location(gym_leader, world.player).item.name
if item_name.startswith("TM"):
try:
tm = int(item_name[2:4])
move = poke_data.moves[self.local_tms[tm - 1]]["id"]
move = poke_data.moves[world.local_tms[tm - 1]]["id"]
data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
except KeyError:
pass
def set_trade_mon(address, loc):
mon = self.multiworld.get_location(loc, self.player).item.name
mon = world.multiworld.get_location(loc, world.player).item.name
data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"]
self.trade_mons[address] = mon
world.trade_mons[address] = mon
if game_version == "red":
set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5")
@@ -282,10 +325,10 @@ def generate_output(self, output_directory: str):
set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9")
set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4")
data[rom_addresses['Fly_Location']] = self.fly_map_code
data[rom_addresses['Map_Fly_Location']] = self.town_map_fly_map_code
data[rom_addresses['Fly_Location']] = world.fly_map_code
data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code
if self.multiworld.fix_combat_bugs[self.player]:
if world.options.fix_combat_bugs:
data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1
data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z
data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de)
@@ -298,25 +341,25 @@ def generate_output(self, output_directory: str):
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1
if self.multiworld.poke_doll_skip[self.player] == "in_logic":
if world.options.poke_doll_skip == "in_logic":
data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop
data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop
data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop
if self.multiworld.bicycle_gate_skips[self.player] == "patched":
if world.options.bicycle_gate_skips == "patched":
data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop
data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop
data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop
data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop
if self.multiworld.door_shuffle[self.player]:
if world.options.door_shuffle:
data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F
if self.multiworld.all_elevators_locked[self.player]:
if world.options.all_elevators_locked:
data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz
data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz
if self.multiworld.tea[self.player].value:
if world.options.tea:
data[rom_addresses["Option_Tea"]] = 1
data[rom_addresses["Guard_Drink_List"]] = 0x54
data[rom_addresses["Guard_Drink_List"] + 1] = 0
@@ -325,90 +368,94 @@ def generate_output(self, output_directory: str):
"<PARA>Oh wait there,<LINE>the road's closed.<DONE>"),
rom_addresses["Text_Saffron_Gate"])
data[rom_addresses["Tea_Key_Item_A"]] = 0x28 # jr .z
data[rom_addresses["Tea_Key_Item_B"]] = 0x28 # jr .z
data[rom_addresses["Tea_Key_Item_C"]] = 0x28 # jr .z
data[rom_addresses["Fossils_Needed_For_Second_Item"]] = (
self.multiworld.second_fossil_check_condition[self.player].value)
world.options.second_fossil_check_condition.value)
data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value)
data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value)
if self.multiworld.extra_key_items[self.player]:
if world.options.extra_key_items:
data[rom_addresses['Option_Extra_Key_Items_A']] = 1
data[rom_addresses['Option_Extra_Key_Items_B']] = 1
data[rom_addresses['Option_Extra_Key_Items_C']] = 1
data[rom_addresses['Option_Extra_Key_Items_D']] = 1
data[rom_addresses["Option_Split_Card_Key"]] = self.multiworld.split_card_key[self.player].value
data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55)
data[rom_addresses["Option_Cerulean_Cave_Badges"]] = self.multiworld.cerulean_cave_badges_condition[self.player].value
data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = self.multiworld.cerulean_cave_key_items_condition[self.player].total
write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_badges_condition[self.player].value)), rom_addresses["Text_Cerulean_Cave_Badges"])
write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_key_items_condition[self.player].total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"])
data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.multiworld.minimum_steps_between_encounters[self.player].value
data[rom_addresses['Option_Route23_Badges']] = self.multiworld.victory_road_condition[self.player].value
data[rom_addresses['Option_Victory_Road_Badges']] = self.multiworld.route_22_gate_condition[self.player].value
data[rom_addresses['Option_Elite_Four_Pokedex']] = self.multiworld.elite_four_pokedex_condition[self.player].total
data[rom_addresses['Option_Elite_Four_Key_Items']] = self.multiworld.elite_four_key_items_condition[self.player].total
data[rom_addresses['Option_Elite_Four_Badges']] = self.multiworld.elite_four_badges_condition[self.player].value
write_bytes(data, encode_text(str(self.multiworld.elite_four_badges_condition[self.player].value)), rom_addresses["Text_Elite_Four_Badges"])
write_bytes(data, encode_text(str(self.multiworld.elite_four_key_items_condition[self.player].total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"])
write_bytes(data, encode_text(str(self.multiworld.elite_four_pokedex_condition[self.player].total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"])
write_bytes(data, encode_text(str(self.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"])
data[rom_addresses["Option_Split_Card_Key"]] = world.options.split_card_key.value
data[rom_addresses["Option_Blind_Trainers"]] = round(world.options.blind_trainers.value * 2.55)
data[rom_addresses["Option_Cerulean_Cave_Badges"]] = world.options.cerulean_cave_badges_condition.value
data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = world.options.cerulean_cave_key_items_condition.total
write_bytes(data, encode_text(str(world.options.cerulean_cave_badges_condition.value)), rom_addresses["Text_Cerulean_Cave_Badges"])
write_bytes(data, encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"])
data[rom_addresses['Option_Encounter_Minimum_Steps']] = world.options.minimum_steps_between_encounters.value
data[rom_addresses['Option_Route23_Badges']] = world.options.victory_road_condition.value
data[rom_addresses['Option_Victory_Road_Badges']] = world.options.route_22_gate_condition.value
data[rom_addresses['Option_Elite_Four_Pokedex']] = world.options.elite_four_pokedex_condition.total
data[rom_addresses['Option_Elite_Four_Key_Items']] = world.options.elite_four_key_items_condition.total
data[rom_addresses['Option_Elite_Four_Badges']] = world.options.elite_four_badges_condition.value
write_bytes(data, encode_text(str(world.options.elite_four_badges_condition.value)), rom_addresses["Text_Elite_Four_Badges"])
write_bytes(data, encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"])
write_bytes(data, encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"])
write_bytes(data, encode_text(str(world.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"])
data[rom_addresses['Option_Viridian_Gym_Badges']] = self.multiworld.viridian_gym_condition[self.player].value
data[rom_addresses['Option_EXP_Modifier']] = self.multiworld.exp_modifier[self.player].value
if not self.multiworld.require_item_finder[self.player]:
data[rom_addresses['Option_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value
data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value
if not world.options.require_item_finder:
data[rom_addresses['Option_Itemfinder']] = 0 # nop
if self.multiworld.extra_strength_boulders[self.player]:
if world.options.extra_strength_boulders:
for i in range(0, 3):
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
if self.multiworld.extra_key_items[self.player]:
if world.options.extra_key_items:
for i in range(0, 4):
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
if self.multiworld.old_man[self.player] == "open_viridian_city":
if world.options.old_man == "open_viridian_city":
data[rom_addresses['Option_Old_Man']] = 0x11
data[rom_addresses['Option_Old_Man_Lying']] = 0x15
data[rom_addresses['Option_Route3_Guard_B']] = self.multiworld.route_3_condition[self.player].value
if self.multiworld.route_3_condition[self.player] == "open":
data[rom_addresses['Option_Route3_Guard_B']] = world.options.route_3_condition.value
if world.options.route_3_condition == "open":
data[rom_addresses['Option_Route3_Guard_A']] = 0x11
if not self.multiworld.robbed_house_officer[self.player]:
if not world.options.robbed_house_officer:
data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15
data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11
if self.multiworld.require_pokedex[self.player]:
if world.options.require_pokedex:
data[rom_addresses["Require_Pokedex_A"]] = 1
data[rom_addresses["Require_Pokedex_B"]] = 1
data[rom_addresses["Require_Pokedex_C"]] = 1
else:
data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr
if self.multiworld.dexsanity[self.player]:
if world.options.dexsanity:
data[rom_addresses["Option_Dexsanity_A"]] = 1
data[rom_addresses["Option_Dexsanity_B"]] = 1
if self.multiworld.all_pokemon_seen[self.player]:
if world.options.all_pokemon_seen:
data[rom_addresses["Option_Pokedex_Seen"]] = 1
money = str(self.multiworld.starting_money[self.player].value).zfill(6)
money = str(world.options.starting_money.value).zfill(6)
data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16)
data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16)
data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16)
data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text(
str(self.multiworld.viridian_gym_condition[self.player].value))[0]
str(world.options.viridian_gym_condition.value))[0]
data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text(
str(self.multiworld.victory_road_condition[self.player].value))[0]
str(world.options.victory_road_condition.value))[0]
data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text(
str(self.multiworld.victory_road_condition[self.player].value))[0]
str(world.options.victory_road_condition.value))[0]
data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text(
str(self.multiworld.victory_road_condition[self.player].value))[0]
str(world.options.victory_road_condition.value))[0]
data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text(
str(self.multiworld.victory_road_condition[self.player].value))[0]
str(world.options.victory_road_condition.value))[0]
data[rom_addresses["Text_Badges_Needed"]] = encode_text(
str(self.multiworld.elite_four_badges_condition[self.player].value))[0]
str(world.options.elite_four_badges_condition.value))[0]
write_bytes(data, encode_text(
" ".join(self.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", self.player).item.name.upper().split()[1:])),
" ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])),
rom_addresses["Text_Magikarp_Salesman"])
if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0:
if world.options.badges_needed_for_hm_moves.value == 0:
for hm_move in poke_data.hm_moves:
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
rom_addresses["HM_" + hm_move + "_Badge_a"])
elif self.extra_badges:
elif world.extra_badges:
written_badges = {}
for hm_move, badge in self.extra_badges.items():
for hm_move, badge in world.extra_badges.items():
data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
@@ -427,7 +474,7 @@ def generate_output(self, output_directory: str):
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
type_loc = rom_addresses["Type_Chart"]
for matchup in self.type_chart:
for matchup in world.type_chart:
if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10
data[type_loc] = poke_data.type_ids[matchup[0]]
data[type_loc + 1] = poke_data.type_ids[matchup[1]]
@@ -437,52 +484,49 @@ def generate_output(self, output_directory: str):
data[type_loc + 1] = 0xFF
data[type_loc + 2] = 0xFF
if self.multiworld.normalize_encounter_chances[self.player].value:
if world.options.normalize_encounter_chances.value:
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
for i, chance in enumerate(chances):
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
for mon, mon_data in self.local_poke_data.items():
for mon, mon_data in world.local_poke_data.items():
if mon == "Mew":
address = rom_addresses["Base_Stats_Mew"]
else:
address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1))
data[address + 1] = self.local_poke_data[mon]["hp"]
data[address + 2] = self.local_poke_data[mon]["atk"]
data[address + 3] = self.local_poke_data[mon]["def"]
data[address + 4] = self.local_poke_data[mon]["spd"]
data[address + 5] = self.local_poke_data[mon]["spc"]
data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]]
data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]]
data[address + 8] = self.local_poke_data[mon]["catch rate"]
data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"]
data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"]
data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"]
data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"]
write_bytes(data, self.local_poke_data[mon]["tms"], address + 20)
if mon in self.learnsets and self.learnsets[mon]:
data[address + 1] = world.local_poke_data[mon]["hp"]
data[address + 2] = world.local_poke_data[mon]["atk"]
data[address + 3] = world.local_poke_data[mon]["def"]
data[address + 4] = world.local_poke_data[mon]["spd"]
data[address + 5] = world.local_poke_data[mon]["spc"]
data[address + 6] = poke_data.type_ids[world.local_poke_data[mon]["type1"]]
data[address + 7] = poke_data.type_ids[world.local_poke_data[mon]["type2"]]
data[address + 8] = world.local_poke_data[mon]["catch rate"]
data[address + 15] = poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"]
data[address + 16] = poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"]
data[address + 17] = poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"]
data[address + 18] = poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"]
write_bytes(data, world.local_poke_data[mon]["tms"], address + 20)
if mon in world.learnsets and world.learnsets[mon]:
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
for i, move in enumerate(self.learnsets[mon]):
for i, move in enumerate(world.learnsets[mon]):
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value
data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value
data[rom_addresses["Option_Aide_Rt15"]] = self.multiworld.oaks_aide_rt_15[self.player].value
data[rom_addresses["Option_Aide_Rt2"]] = world.options.oaks_aide_rt_2.value
data[rom_addresses["Option_Aide_Rt11"]] = world.options.oaks_aide_rt_11.value
data[rom_addresses["Option_Aide_Rt15"]] = world.options.oaks_aide_rt_15.value
if self.multiworld.safari_zone_normal_battles[self.player].value == 1:
if world.options.safari_zone_normal_battles.value == 1:
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
if self.multiworld.reusable_tms[self.player].value:
if world.options.reusable_tms.value:
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
for i in range(1, 10):
data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value
data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value)
data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value)
if self.multiworld.better_shops[self.player]:
if world.options.better_shops:
inventory = ["Poke Ball", "Great Ball", "Ultra Ball"]
if self.multiworld.better_shops[self.player].value == 2:
if world.options.better_shops.value == 2:
inventory.append("Master Ball")
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote",
"Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel",
@@ -492,30 +536,30 @@ def generate_output(self, output_directory: str):
shop_data.append(0xFF)
for shop in range(1, 11):
write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"])
if self.multiworld.stonesanity[self.player]:
if world.options.stonesanity:
write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"])
price = str(self.multiworld.master_ball_price[self.player].value).zfill(6)
price = str(world.options.master_ball_price.value).zfill(6)
price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)])
write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird
for item in reversed(self.multiworld.precollected_items[self.player]):
for item in reversed(world.multiworld.precollected_items[world.player]):
if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255:
data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1
set_mon_palettes(self, random, data)
set_mon_palettes(world, random, data)
for move_data in self.local_move_data.values():
for move_data in world.local_move_data.values():
if move_data["id"] == 0:
continue
address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6)
write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"],
poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address)
TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms])
TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms])
write_bytes(data, TM_IDs, rom_addresses["TM_Moves"])
if self.multiworld.randomize_rock_tunnel[self.player]:
if world.options.randomize_rock_tunnel:
seed = randomize_rock_tunnel(data, random)
write_bytes(data, encode_text(f"SEED: <LINE>{seed}"), rom_addresses["Text_Rock_Tunnel_Sign"])
@@ -524,44 +568,44 @@ def generate_output(self, output_directory: str):
data[rom_addresses['Title_Mon_First']] = mons.pop()
for mon in range(0, 16):
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
if self.multiworld.game_version[self.player].value:
mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name
else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name else
2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3)
if world.options.game_version.value:
mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name
else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else
2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3)
else:
mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name
else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name else
2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3)
write_bytes(data, encode_text(self.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed'])
mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name
else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else
2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3)
write_bytes(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed'])
slot_name = self.multiworld.player_name[self.player]
slot_name = world.multiworld.player_name[world.player]
slot_name.replace("@", " ")
slot_name.replace("<", " ")
slot_name.replace(">", " ")
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
if self.trainer_name == "choose_in_game":
if world.trainer_name == "choose_in_game":
data[rom_addresses["Skip_Player_Name"]] = 0
else:
write_bytes(data, self.trainer_name, rom_addresses['Player_Name'])
if self.rival_name == "choose_in_game":
write_bytes(data, world.trainer_name, rom_addresses['Player_Name'])
if world.rival_name == "choose_in_game":
data[rom_addresses["Skip_Rival_Name"]] = 0
else:
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
write_bytes(data, world.rival_name, rom_addresses['Rival_Name'])
data[0xFF00] = 2 # client compatibility version
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
'utf8')[:21]
rom_name.extend([0] * (21 - len(rom_name)))
write_bytes(data, rom_name, 0xFFC6)
write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB)
write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0)
write_bytes(data, world.multiworld.seed_name.encode(), 0xFFDB)
write_bytes(data, world.multiworld.player_name[world.player].encode(), 0xFFF0)
self.finished_level_scaling.wait()
world.finished_level_scaling.wait()
write_quizzes(self, data, random)
write_quizzes(world, data, random)
for location in self.multiworld.get_locations(self.player):
for location in world.multiworld.get_locations(world.player):
if location.party_data:
for party in location.party_data:
if not isinstance(party["party_address"], list):
@@ -588,7 +632,7 @@ def generate_output(self, output_directory: str):
continue
elif location.rom_address is None:
continue
if location.item and location.item.player == self.player:
if location.item and location.item.player == world.player:
if location.rom_address:
rom_address = location.rom_address
if not isinstance(rom_address, list):
@@ -599,7 +643,7 @@ def generate_output(self, output_directory: str):
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
else:
item_id = self.item_name_to_id[location.item.name] - 172000000
item_id = world.item_name_to_id[location.item.name] - 172000000
if item_id > 255:
item_id -= 256
data[address] = item_id
@@ -613,18 +657,18 @@ def generate_output(self, output_directory: str):
for address in rom_address:
data[address] = 0x2C # AP Item
outfilepname = f'_P{self.player}'
outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" \
if self.multiworld.player_name[self.player] != 'Player%d' % self.player else ''
rompath = os.path.join(output_directory, f'AP_{self.multiworld.seed_name}{outfilepname}.gb')
outfilepname = f'_P{world.player}'
outfilepname += f"_{world.multiworld.get_file_safe_player_name(world.player).replace(' ', '_')}" \
if world.multiworld.player_name[world.player] != 'Player%d' % world.player else ''
rompath = os.path.join(output_directory, f'AP_{world.multiworld.seed_name}{outfilepname}.gb')
with open(rompath, 'wb') as outfile:
outfile.write(data)
if self.multiworld.game_version[self.player].current_key == "red":
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
if world.options.game_version.current_key == "red":
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=world.player,
player_name=world.multiworld.player_name[world.player], patched_path=rompath)
else:
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=world.player,
player_name=world.multiworld.player_name[world.player], patched_path=rompath)
patch.write()
os.unlink(rompath)
+222 -228
View File
@@ -1,10 +1,9 @@
rom_addresses = {
"Option_Encounter_Minimum_Steps": 0x3c1,
"Option_Pitch_Black_Rock_Tunnel": 0x76a,
"Option_Blind_Trainers": 0x30d5,
"Option_Trainersanity1": 0x3165,
"Option_Split_Card_Key": 0x3e1e,
"Option_Fix_Combat_Bugs": 0x3e1f,
"Option_Blind_Trainers": 0x32f0,
"Option_Split_Card_Key": 0x3e19,
"Option_Fix_Combat_Bugs": 0x3e1a,
"Option_Lose_Money": 0x40d4,
"Base_Stats_Mew": 0x4260,
"Title_Mon_First": 0x4373,
@@ -115,9 +114,10 @@ rom_addresses = {
"HM_Strength_Badge_b": 0x131ed,
"HM_Flash_Badge_a": 0x131fc,
"HM_Flash_Badge_b": 0x13201,
"Trainer_Screen_Total_Key_Items": 0x135dc,
"TM_Moves": 0x137b1,
"Encounter_Chances": 0x13950,
"Tea_Key_Item_A": 0x135ac,
"Trainer_Screen_Total_Key_Items": 0x1361b,
"TM_Moves": 0x137f0,
"Encounter_Chances": 0x1398f,
"Warps_CeladonCity": 0x18026,
"Warps_PalletTown": 0x182c7,
"Warps_ViridianCity": 0x18388,
@@ -128,52 +128,54 @@ rom_addresses = {
"Option_Viridian_Gym_Badges": 0x1901d,
"Event_Sleepy_Guy": 0x191d1,
"Option_Route3_Guard_B": 0x1928a,
"Starter2_K": 0x19611,
"Starter3_K": 0x19619,
"Event_Rocket_Thief": 0x19733,
"Option_Cerulean_Cave_Badges": 0x19861,
"Option_Cerulean_Cave_Key_Items": 0x19868,
"Text_Cerulean_Cave_Badges": 0x198d7,
"Text_Cerulean_Cave_Key_Items": 0x198e5,
"Event_Stranded_Man": 0x19b3c,
"Event_Rivals_Sister": 0x19d0f,
"Warps_BluesHouse": 0x19d65,
"Warps_VermilionTradeHouse": 0x19dbc,
"Require_Pokedex_D": 0x19e53,
"Option_Elite_Four_Key_Items": 0x19e9d,
"Option_Elite_Four_Pokedex": 0x19ea4,
"Option_Elite_Four_Badges": 0x19eab,
"Text_Elite_Four_Badges": 0x19f47,
"Text_Elite_Four_Key_Items": 0x19f51,
"Text_Elite_Four_Pokedex": 0x19f64,
"Shop10": 0x1a018,
"Warps_IndigoPlateauLobby": 0x1a044,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a16c,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a17a,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a188,
"Event_SKC4F": 0x1a19b,
"Warps_SilphCo4F": 0x1a21d,
"Missable_Silph_Co_4F_Item_1": 0x1a25d,
"Missable_Silph_Co_4F_Item_2": 0x1a264,
"Missable_Silph_Co_4F_Item_3": 0x1a26b,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a3c3,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a3d1,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a3df,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a3ed,
"Event_SKC5F": 0x1a400,
"Warps_SilphCo5F": 0x1a4aa,
"Missable_Silph_Co_5F_Item_1": 0x1a4f2,
"Missable_Silph_Co_5F_Item_2": 0x1a4f9,
"Missable_Silph_Co_5F_Item_3": 0x1a500,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a630,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a63e,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a64c,
"Event_SKC6F": 0x1a66d,
"Warps_SilphCo6F": 0x1a74b,
"Missable_Silph_Co_6F_Item_1": 0x1a79b,
"Missable_Silph_Co_6F_Item_2": 0x1a7a2,
"Path_Pallet_Oak": 0x1a928,
"Path_Pallet_Player": 0x1a935,
"Starter2_K": 0x19618,
"Starter3_K": 0x19620,
"Event_Rocket_Thief": 0x1973a,
"Tea_Key_Item_C": 0x1988f,
"Option_Cerulean_Cave_Badges": 0x198a0,
"Option_Cerulean_Cave_Key_Items": 0x198a7,
"Text_Cerulean_Cave_Badges": 0x19916,
"Text_Cerulean_Cave_Key_Items": 0x19924,
"Event_Stranded_Man": 0x19b7b,
"Event_Rivals_Sister": 0x19d4e,
"Warps_BluesHouse": 0x19da4,
"Warps_VermilionTradeHouse": 0x19dfb,
"Require_Pokedex_D": 0x19e99,
"Tea_Key_Item_B": 0x19f13,
"Option_Elite_Four_Key_Items": 0x19f1b,
"Option_Elite_Four_Pokedex": 0x19f22,
"Option_Elite_Four_Badges": 0x19f29,
"Text_Elite_Four_Badges": 0x19fc5,
"Text_Elite_Four_Key_Items": 0x19fcf,
"Text_Elite_Four_Pokedex": 0x19fe2,
"Shop10": 0x1a096,
"Warps_IndigoPlateauLobby": 0x1a0c2,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a1ea,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a1f8,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a206,
"Event_SKC4F": 0x1a219,
"Warps_SilphCo4F": 0x1a29b,
"Missable_Silph_Co_4F_Item_1": 0x1a2db,
"Missable_Silph_Co_4F_Item_2": 0x1a2e2,
"Missable_Silph_Co_4F_Item_3": 0x1a2e9,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a441,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a44f,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a45d,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a46b,
"Event_SKC5F": 0x1a47e,
"Warps_SilphCo5F": 0x1a528,
"Missable_Silph_Co_5F_Item_1": 0x1a570,
"Missable_Silph_Co_5F_Item_2": 0x1a577,
"Missable_Silph_Co_5F_Item_3": 0x1a57e,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a6ae,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a6bc,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a6ca,
"Event_SKC6F": 0x1a6eb,
"Warps_SilphCo6F": 0x1a7c9,
"Missable_Silph_Co_6F_Item_1": 0x1a819,
"Missable_Silph_Co_6F_Item_2": 0x1a820,
"Path_Pallet_Oak": 0x1a9a6,
"Path_Pallet_Player": 0x1a9b3,
"Warps_CinnabarIsland": 0x1c026,
"Warps_Route1": 0x1c0e9,
"Option_Extra_Key_Items_B": 0x1ca46,
@@ -191,75 +193,75 @@ rom_addresses = {
"Starter2_E": 0x1d2f7,
"Starter3_E": 0x1d2ff,
"Event_Pokedex": 0x1d363,
"Event_Oaks_Gift": 0x1d393,
"Starter2_P": 0x1d481,
"Starter3_P": 0x1d489,
"Warps_OaksLab": 0x1d6af,
"Event_Pokemart_Quest": 0x1d76b,
"Shop1": 0x1d795,
"Warps_ViridianMart": 0x1d7d8,
"Warps_ViridianSchoolHouse": 0x1d82b,
"Warps_ViridianNicknameHouse": 0x1d889,
"Warps_PewterNidoranHouse": 0x1d8e4,
"Warps_PewterSpeechHouse": 0x1d927,
"Warps_CeruleanTrashedHouse": 0x1d98d,
"Warps_CeruleanTradeHouse": 0x1d9de,
"Event_Bicycle_Shop": 0x1da2f,
"Bike_Shop_Item_Display": 0x1da8a,
"Warps_BikeShop": 0x1db45,
"Event_Fuji": 0x1dbfd,
"Warps_MrFujisHouse": 0x1dc44,
"Warps_LavenderCuboneHouse": 0x1dcc0,
"Warps_NameRatersHouse": 0x1ddae,
"Warps_VermilionPidgeyHouse": 0x1ddf8,
"Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de4e,
"Warps_VermilionDock": 0x1de70,
"Static_Encounter_Mew": 0x1de7e,
"Gift_Eevee": 0x1def7,
"Warps_CeladonMansionRoofHouse": 0x1df0e,
"Shop7": 0x1df49,
"Warps_FuchsiaMart": 0x1df74,
"Warps_SaffronPidgeyHouse": 0x1dfdd,
"Event_Mr_Psychic": 0x1e020,
"Warps_MrPsychicsHouse": 0x1e05d,
"Warps_DiglettsCaveRoute2": 0x1e092,
"Warps_Route2TradeHouse": 0x1e0da,
"Warps_Route5Gate": 0x1e1db,
"Warps_Route6Gate": 0x1e2ad,
"Warps_Route7Gate": 0x1e383,
"Warps_Route8Gate": 0x1e454,
"Warps_UndergroundPathRoute8": 0x1e4a5,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e511,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e51f,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e52d,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e53b,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e549,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e557,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e565,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e573,
"Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e581,
"Warps_PowerPlant": 0x1e5de,
"Static_Encounter_Voltorb_A": 0x1e5f0,
"Static_Encounter_Voltorb_B": 0x1e5f8,
"Static_Encounter_Voltorb_C": 0x1e600,
"Static_Encounter_Electrode_A": 0x1e608,
"Static_Encounter_Voltorb_D": 0x1e610,
"Static_Encounter_Voltorb_E": 0x1e618,
"Static_Encounter_Electrode_B": 0x1e620,
"Static_Encounter_Voltorb_F": 0x1e628,
"Static_Encounter_Zapdos": 0x1e630,
"Missable_Power_Plant_Item_1": 0x1e638,
"Missable_Power_Plant_Item_2": 0x1e63f,
"Missable_Power_Plant_Item_3": 0x1e646,
"Missable_Power_Plant_Item_4": 0x1e64d,
"Missable_Power_Plant_Item_5": 0x1e654,
"Warps_DiglettsCaveRoute11": 0x1e7e9,
"Event_Rt16_House_Woman": 0x1e827,
"Warps_Route16FlyHouse": 0x1e870,
"Option_Victory_Road_Badges": 0x1e8f3,
"Warps_Route22Gate": 0x1e9e3,
"Event_Bill": 0x1eb24,
"Warps_BillsHouse": 0x1eb83,
"Event_Oaks_Gift": 0x1d398,
"Starter2_P": 0x1d486,
"Starter3_P": 0x1d48e,
"Warps_OaksLab": 0x1d6b4,
"Event_Pokemart_Quest": 0x1d770,
"Shop1": 0x1d79a,
"Warps_ViridianMart": 0x1d7dd,
"Warps_ViridianSchoolHouse": 0x1d830,
"Warps_ViridianNicknameHouse": 0x1d88e,
"Warps_PewterNidoranHouse": 0x1d8e9,
"Warps_PewterSpeechHouse": 0x1d92c,
"Warps_CeruleanTrashedHouse": 0x1d992,
"Warps_CeruleanTradeHouse": 0x1d9e3,
"Event_Bicycle_Shop": 0x1da34,
"Bike_Shop_Item_Display": 0x1da8f,
"Warps_BikeShop": 0x1db4a,
"Event_Fuji": 0x1dc02,
"Warps_MrFujisHouse": 0x1dc49,
"Warps_LavenderCuboneHouse": 0x1dcc5,
"Warps_NameRatersHouse": 0x1ddb3,
"Warps_VermilionPidgeyHouse": 0x1ddfd,
"Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de53,
"Warps_VermilionDock": 0x1de75,
"Static_Encounter_Mew": 0x1de83,
"Gift_Eevee": 0x1defc,
"Warps_CeladonMansionRoofHouse": 0x1df13,
"Shop7": 0x1df4e,
"Warps_FuchsiaMart": 0x1df79,
"Warps_SaffronPidgeyHouse": 0x1dfe2,
"Event_Mr_Psychic": 0x1e025,
"Warps_MrPsychicsHouse": 0x1e062,
"Warps_DiglettsCaveRoute2": 0x1e097,
"Warps_Route2TradeHouse": 0x1e0df,
"Warps_Route5Gate": 0x1e1e0,
"Warps_Route6Gate": 0x1e2b2,
"Warps_Route7Gate": 0x1e388,
"Warps_Route8Gate": 0x1e459,
"Warps_UndergroundPathRoute8": 0x1e4aa,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e516,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e524,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e532,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e540,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e54e,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e55c,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e56a,
"Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e578,
"Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e586,
"Warps_PowerPlant": 0x1e5e3,
"Static_Encounter_Voltorb_A": 0x1e5f5,
"Static_Encounter_Voltorb_B": 0x1e5fd,
"Static_Encounter_Voltorb_C": 0x1e605,
"Static_Encounter_Electrode_A": 0x1e60d,
"Static_Encounter_Voltorb_D": 0x1e615,
"Static_Encounter_Voltorb_E": 0x1e61d,
"Static_Encounter_Electrode_B": 0x1e625,
"Static_Encounter_Voltorb_F": 0x1e62d,
"Static_Encounter_Zapdos": 0x1e635,
"Missable_Power_Plant_Item_1": 0x1e63d,
"Missable_Power_Plant_Item_2": 0x1e644,
"Missable_Power_Plant_Item_3": 0x1e64b,
"Missable_Power_Plant_Item_4": 0x1e652,
"Missable_Power_Plant_Item_5": 0x1e659,
"Warps_DiglettsCaveRoute11": 0x1e7ee,
"Event_Rt16_House_Woman": 0x1e82c,
"Warps_Route16FlyHouse": 0x1e875,
"Option_Victory_Road_Badges": 0x1e8f8,
"Warps_Route22Gate": 0x1e9e8,
"Event_Bill": 0x1eb29,
"Warps_BillsHouse": 0x1eb88,
"Starter1_O": 0x372b0,
"Starter2_O": 0x372b4,
"Starter3_O": 0x372b8,
@@ -1470,74 +1472,73 @@ rom_addresses = {
"Trainersanity_EVENT_BEAT_POKEMONTOWER_5_TRAINER_3_ITEM": 0x609ea,
"Warps_PokemonTower5F": 0x60a5e,
"Missable_Pokemon_Tower_5F_Item": 0x60a92,
"Option_Trainersanity2": 0x60b2a,
"Ghost_Battle1": 0x60b83,
"Ghost_Battle_Level": 0x60b88,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c25,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c33,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c41,
"Ghost_Battle2": 0x60c69,
"Warps_PokemonTower6F": 0x60cbe,
"Missable_Pokemon_Tower_6F_Item_1": 0x60ce4,
"Missable_Pokemon_Tower_6F_Item_2": 0x60ceb,
"Entrance_Shuffle_Fuji_Warp": 0x60deb,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60edf,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60eed,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60efb,
"Warps_PokemonTower7F": 0x60f8b,
"Warps_CeladonMart1F": 0x61033,
"Gift_Aerodactyl": 0x610f5,
"Gift_Omanyte": 0x610f9,
"Gift_Kabuto": 0x610fd,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611de,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611ec,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611fa,
"Warps_ViridianForest": 0x61273,
"Missable_Viridian_Forest_Item_1": 0x612c1,
"Missable_Viridian_Forest_Item_2": 0x612c8,
"Missable_Viridian_Forest_Item_3": 0x612cf,
"Warps_SSAnne1F": 0x61310,
"Starter2_M": 0x614e5,
"Starter3_M": 0x614ed,
"Warps_SSAnne2F": 0x615ab,
"Warps_SSAnneB1F": 0x616c9,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x61771,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x6177f,
"Warps_SSAnneBow": 0x617c6,
"Warps_SSAnneKitchen": 0x618b6,
"Event_SS_Anne_Captain": 0x6194e,
"Warps_SSAnneCaptainsRoom": 0x619d5,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a3d,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a4b,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a59,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a67,
"Warps_SSAnne1FRooms": 0x61af7,
"Missable_SS_Anne_1F_Item": 0x61b53,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c24,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c32,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c40,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c4e,
"Warps_SSAnne2FRooms": 0x61d2c,
"Missable_SS_Anne_2F_Item_1": 0x61d88,
"Missable_SS_Anne_2F_Item_2": 0x61d9b,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e2c,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e3a,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e48,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e56,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e64,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e72,
"Warps_SSAnneB1FRooms": 0x61f20,
"Missable_SS_Anne_B1F_Item_1": 0x61f8a,
"Missable_SS_Anne_B1F_Item_2": 0x61f91,
"Missable_SS_Anne_B1F_Item_3": 0x61f98,
"Warps_UndergroundPathNorthSouth": 0x61fd5,
"Warps_UndergroundPathWestEast": 0x61ff9,
"Warps_DiglettsCave": 0x6201d,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62358,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62366,
"Event_Silph_Co_President": 0x62373,
"Event_SKC11F": 0x623bd,
"Warps_SilphCo11F": 0x62446,
"Ghost_Battle1": 0x60b93,
"Ghost_Battle_Level": 0x60b98,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c35,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c43,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c51,
"Ghost_Battle2": 0x60c79,
"Warps_PokemonTower6F": 0x60cce,
"Missable_Pokemon_Tower_6F_Item_1": 0x60cf4,
"Missable_Pokemon_Tower_6F_Item_2": 0x60cfb,
"Entrance_Shuffle_Fuji_Warp": 0x60dfb,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60eef,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60efd,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60f0b,
"Warps_PokemonTower7F": 0x60f9b,
"Warps_CeladonMart1F": 0x61043,
"Gift_Aerodactyl": 0x61105,
"Gift_Omanyte": 0x61109,
"Gift_Kabuto": 0x6110d,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x61209,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x61217,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x61225,
"Warps_ViridianForest": 0x6129e,
"Missable_Viridian_Forest_Item_1": 0x612ec,
"Missable_Viridian_Forest_Item_2": 0x612f3,
"Missable_Viridian_Forest_Item_3": 0x612fa,
"Warps_SSAnne1F": 0x6133b,
"Starter2_M": 0x61510,
"Starter3_M": 0x61518,
"Warps_SSAnne2F": 0x615d6,
"Warps_SSAnneB1F": 0x616f4,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x6179c,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x617aa,
"Warps_SSAnneBow": 0x617f1,
"Warps_SSAnneKitchen": 0x618e1,
"Event_SS_Anne_Captain": 0x61979,
"Warps_SSAnneCaptainsRoom": 0x61a00,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a68,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a76,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a84,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a92,
"Warps_SSAnne1FRooms": 0x61b22,
"Missable_SS_Anne_1F_Item": 0x61b7e,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c4f,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c5d,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c6b,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c79,
"Warps_SSAnne2FRooms": 0x61d57,
"Missable_SS_Anne_2F_Item_1": 0x61db3,
"Missable_SS_Anne_2F_Item_2": 0x61dc6,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e57,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e65,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e73,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e81,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e8f,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e9d,
"Warps_SSAnneB1FRooms": 0x61f4b,
"Missable_SS_Anne_B1F_Item_1": 0x61fb5,
"Missable_SS_Anne_B1F_Item_2": 0x61fbc,
"Missable_SS_Anne_B1F_Item_3": 0x61fc3,
"Warps_UndergroundPathNorthSouth": 0x62000,
"Warps_UndergroundPathWestEast": 0x62024,
"Warps_DiglettsCave": 0x62048,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62383,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62391,
"Event_Silph_Co_President": 0x6239e,
"Event_SKC11F": 0x623e8,
"Warps_SilphCo11F": 0x62471,
"Ghost_Battle4": 0x708e1,
"Town_Map_Order": 0x70f0f,
"Town_Map_Coords": 0x71381,
@@ -1589,44 +1590,37 @@ rom_addresses = {
"Warps_FuchsiaMeetingRoom": 0x75879,
"Badge_Cinnabar_Gym": 0x759de,
"Event_Cinnabar_Gym": 0x759f2,
"Option_Trainersanity4": 0x75ace,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75ada,
"Option_Trainersanity3": 0x75b1e,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2a,
"Option_Trainersanity5": 0x75b85,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b91,
"Option_Trainersanity6": 0x75bd5,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be1,
"Option_Trainersanity7": 0x75c25,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c31,
"Option_Trainersanity8": 0x75c75,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c81,
"Option_Trainersanity9": 0x75cc5,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cd1,
"Warps_CinnabarGym": 0x75d1b,
"Warps_CinnabarLab": 0x75e02,
"Warps_CinnabarLabTradeRoom": 0x75e94,
"Event_Lab_Scientist": 0x75ee9,
"Warps_CinnabarLabMetronomeRoom": 0x75f35,
"Fossils_Needed_For_Second_Item": 0x75fb6,
"Fossil_Level": 0x76017,
"Event_Dome_Fossil_B": 0x76031,
"Event_Helix_Fossil_B": 0x76051,
"Warps_CinnabarLabFossilRoom": 0x760d2,
"Warps_CinnabarPokecenter": 0x76128,
"Shop8": 0x7616f,
"Warps_CinnabarMart": 0x7619b,
"Warps_CopycatsHouse1F": 0x761ed,
"Starter2_N": 0x762a2,
"Starter3_N": 0x762aa,
"Warps_ChampionsRoom": 0x764d5,
"Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76604,
"Warps_LoreleisRoom": 0x76628,
"Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7675d,
"Warps_BrunosRoom": 0x76781,
"Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768bc,
"Warps_AgathasRoom": 0x768e0,
"Option_Itemfinder": 0x76a33,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75adc,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2e,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b97,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be9,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c3b,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c8d,
"Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cdf,
"Warps_CinnabarGym": 0x75d29,
"Warps_CinnabarLab": 0x75e10,
"Warps_CinnabarLabTradeRoom": 0x75ea2,
"Event_Lab_Scientist": 0x75ef7,
"Warps_CinnabarLabMetronomeRoom": 0x75f43,
"Fossils_Needed_For_Second_Item": 0x75fc4,
"Fossil_Level": 0x76025,
"Event_Dome_Fossil_B": 0x7603f,
"Event_Helix_Fossil_B": 0x7605f,
"Warps_CinnabarLabFossilRoom": 0x760e0,
"Warps_CinnabarPokecenter": 0x76136,
"Shop8": 0x7617d,
"Warps_CinnabarMart": 0x761a9,
"Warps_CopycatsHouse1F": 0x761fb,
"Starter2_N": 0x762b0,
"Starter3_N": 0x762b8,
"Warps_ChampionsRoom": 0x764e3,
"Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76612,
"Warps_LoreleisRoom": 0x76636,
"Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7676b,
"Warps_BrunosRoom": 0x7678f,
"Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768ca,
"Warps_AgathasRoom": 0x768ee,
"Option_Itemfinder": 0x76a41,
"Text_Quiz_A": 0x88806,
"Text_Quiz_B": 0x8893a,
"Text_Quiz_C": 0x88a6e,
+99 -108
View File
@@ -3,7 +3,7 @@ from .items import item_groups
from . import logic
def set_rules(multiworld, player):
def set_rules(multiworld, world, player):
item_rules = {
# Some items do special things when they are passed into the GiveItem function in the game, but
@@ -15,54 +15,46 @@ def set_rules(multiworld, player):
not in i.name)
}
if multiworld.prizesanity[player]:
if world.options.prizesanity:
def prize_rule(i):
return i.player != player or i.name in item_groups["Unique"]
item_rules["Celadon Prize Corner - Item Prize 1"] = prize_rule
item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule
item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule
if multiworld.accessibility[player] != "full":
multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item:
item.name == "Bike Voucher"
and item.player == player)
multiworld.get_location("Fuchsia Warden's House - Safari Zone Warden", player).always_allow = (lambda state, item:
item.name == "Gold Teeth" and
item.player == player)
access_rules = {
"Rival's House - Rival's Sister": lambda state: state.has("Oak's Parcel", player),
"Oak's Lab - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player),
"Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, player) or logic.can_surf(state, player),
"Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_2[player].value + 5, player),
"Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, world, player) or logic.can_surf(state, world, player),
"Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_2.value + 5, player),
"Cerulean Bicycle Shop": lambda state: state.has("Bike Voucher", player)
or location_item_name(state, "Cerulean Bicycle Shop", player) == ("Bike Voucher", player),
"Lavender Mr. Fuji's House - Mr. Fuji": lambda state: state.has("Fuji Saved", player),
"Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_11[player].value + 5, player),
"Celadon City - Stranded Man": lambda state: logic.can_surf(state, player),
"Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_11.value + 5, player),
"Celadon City - Stranded Man": lambda state: logic.can_surf(state, world, player),
"Fuchsia Warden's House - Safari Zone Warden": lambda state: state.has("Gold Teeth", player)
or location_item_name(state, "Fuchsia Warden's House - Safari Zone Warden", player) == ("Gold Teeth", player),
"Route 12 - Island Item": lambda state: logic.can_surf(state, player),
"Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_15[player].value + 5, player),
"Route 25 - Item": lambda state: logic.can_cut(state, player),
"Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, player),
"Safari Zone Center - Island Item": lambda state: logic.can_surf(state, player),
"Route 12 - Island Item": lambda state: logic.can_surf(state, world, player),
"Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_15.value + 5, player),
"Route 25 - Item": lambda state: logic.can_cut(state, world, player),
"Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, world, player),
"Safari Zone Center - Island Item": lambda state: logic.can_surf(state, world, player),
"Saffron Copycat's House 2F - Copycat": lambda state: state.has("Buy Poke Doll", player),
"Celadon Game Corner - West Gambler's Gift": lambda state: state.has("Coin Case", player),
"Celadon Game Corner - Center Gambler's Gift": lambda state: state.has("Coin Case", player),
"Celadon Game Corner - East Gambler's Gift": lambda state: state.has("Coin Case", player),
"Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player),
"Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player),
"Celadon Prize Corner - Item Prize 1": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player),
"Celadon Prize Corner - Item Prize 2": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player),
@@ -79,9 +71,9 @@ def set_rules(multiworld, player):
"Cinnabar Lab Fossil Room - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player) and state.has("Cinnabar Island", player),
"Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
"Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
"Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, player) and state.has("Seafoam Boss Boulders", player),
"Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, player),
"Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, player),
"Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Boss Boulders", player),
"Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, world, player),
"Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, world, player),
**{f"Pokemon Tower {floor}F - Wild Pokemon - {slot}": lambda state: state.has("Silph Scope", player) for floor in range(3, 8) for slot in range(1, 11)},
"Pokemon Tower 6F - Restless Soul": lambda state: state.has("Silph Scope", player), # just for level scaling
@@ -103,101 +95,101 @@ def set_rules(multiworld, player):
"Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player),
# # Rock Tunnel
"Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
"Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, world, player),
"Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, world, player),
# Pokédex check
"Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player),
# Hidden items
"Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state,
"Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, world,
player),
"Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, player),
"Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state,
"Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, world, player),
"Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state,
"Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, player),
"S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, player),
"S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, player),
"Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, world, player),
"S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, world, player),
"S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 10 - Hidden Item Behind Rock Tunnel Entrance Cuttable Tree": lambda
state: logic.can_get_hidden_items(state, player) and logic.can_cut(state, player),
"Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, player),
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player),
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, player),
state: logic.can_get_hidden_items(state, world, player) and logic.can_cut(state, world, player),
"Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, world, player),
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player),
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, world, player),
"Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state:
logic.can_get_hidden_items(state, player),
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state,
logic.can_get_hidden_items(state, world, player),
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, player),
"Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, player),
"Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state,
"Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, world, player),
"Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, world,
player),
"Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state,
"Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, world,
player),
"Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player),
"Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, player),
"Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, player),
"Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, player),
"Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, player),
"Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, player),
"Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, player),
"Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player),
"Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player),
"Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player),
"Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, world, player),
"Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, world, player),
"Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, world, player),
"Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, world, player),
"Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, world, player),
"Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, world, player),
"Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player),
"Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player),
# if you can reach any exit boulders, that means you can drop into the water tunnel and auto-surf
"Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, player),
"Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, world, player),
"Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda
state: logic.can_get_hidden_items(state, player),
"Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, player),
"Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state,
state: logic.can_get_hidden_items(state, world, player),
"Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state,
"Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state,
player) and logic.can_surf(state, player),
"Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state,
"Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, world,
player) and logic.can_surf(state, world, player),
"Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, world,
player),
"Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, player),
"Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, player),
"Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player),
"Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player),
"Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, player),
"Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, player),
"Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, player),
"Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, player),
"Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, player),
"Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, world, player),
"Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, world, player),
"Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, world, player),
"Underground Path North South - Hidden Item Near Northern Stairs": lambda
state: logic.can_get_hidden_items(state, player),
state: logic.can_get_hidden_items(state, world, player),
"Underground Path North South - Hidden Item Near Southern Stairs": lambda
state: logic.can_get_hidden_items(state, player),
"Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, player),
"Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, player),
"Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state,
state: logic.can_get_hidden_items(state, world, player),
"Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, world, player),
"Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, world, player),
"Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, player),
"Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, player),
"Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state,
player) and logic.can_surf(state, player),
"Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state,
"Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, world, player),
"Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, world, player),
"Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, world,
player) and logic.can_surf(state, world, player),
"Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, world,
player),
"Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, player),
"Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, world, player),
# Evolutions
"Evolution - Ivysaur": lambda state: state.has("Bulbasaur", player) and logic.evolve_level(state, 16, player),
@@ -281,5 +273,4 @@ def set_rules(multiworld, player):
if loc.name.startswith("Pokedex"):
mon = loc.name.split(" - ")[1]
add_rule(loc, lambda state, i=mon: (state.has("Pokedex", player) or not
state.multiworld.require_pokedex[player]) and (state.has(i, player)
or state.has(f"Static {i}", player)))
world.options.require_pokedex) and (state.has(i, player) or state.has(f"Static {i}", player)))
+7 -7
View File
@@ -97,12 +97,12 @@ class ConfigurableOptionInfo(typing.NamedTuple):
class ColouredMessage:
def __init__(self, text: str = '') -> None:
def __init__(self, text: str = '', *, keep_markup: bool = False) -> None:
self.parts: typing.List[dict] = []
if text:
self(text)
def __call__(self, text: str) -> 'ColouredMessage':
add_json_text(self.parts, text)
self(text, keep_markup=keep_markup)
def __call__(self, text: str, *, keep_markup: bool = False) -> 'ColouredMessage':
add_json_text(self.parts, text, keep_markup=keep_markup)
return self
def coloured(self, text: str, colour: str) -> 'ColouredMessage':
add_json_text(self.parts, text, type="color", color=colour)
@@ -128,7 +128,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
# Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output.
# Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs,
# without having to branch code from CommonClient
self.ctx.on_print_json({"data": [{"text": text}]})
self.ctx.on_print_json({"data": [{"text": text, "keep_markup": True}]})
def _cmd_difficulty(self, difficulty: str = "") -> bool:
"""Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal"""
@@ -257,7 +257,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
print_faction_title()
has_printed_faction_title = True
(ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags)
(" from ").location(item.location, self.ctx.slot)
(" from ").location(item.location, item.player)
(" by ").player(item.player)
).send(self.ctx)
@@ -278,7 +278,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
for item in received_items_of_this_type:
filter_match_count += len(received_items_of_this_type)
(ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags)
(" from ").location(item.location, self.ctx.slot)
(" from ").location(item.location, item.player)
(" by ").player(item.player)
).send(self.ctx)
+15 -1
View File
@@ -1,7 +1,8 @@
from typing import *
import asyncio
from kvui import GameManager, HoverBehavior, ServerToolTip
from NetUtils import JSONMessagePart
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem
@@ -69,6 +70,18 @@ class MissionLayout(GridLayout):
class MissionCategory(GridLayout):
pass
class SC2JSONtoKivyParser(KivyJSONtoTextParser):
def _handle_text(self, node: JSONMessagePart):
if node.get("keep_markup", False):
for ref in node.get("refs", []):
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
self.ref_count += 1
return super(KivyJSONtoTextParser, self)._handle_text(node)
else:
return super()._handle_text(node)
class SC2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
@@ -87,6 +100,7 @@ class SC2Manager(GameManager):
def __init__(self, ctx) -> None:
super().__init__(ctx)
self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx)
def clear_tooltip(self) -> None:
if self.ctx.current_tooltip:
+2 -2
View File
@@ -246,10 +246,10 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int):
regBitS.subregions = [bits_top]
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None):
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None) -> Entrance:
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
sourceRegion.connect(targetRegion, rule=rule)
return sourceRegion.connect(targetRegion, rule=rule)
def create_region(name: str, player: int, world: MultiWorld) -> SM64Region:
+6 -3
View File
@@ -92,9 +92,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict,
connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"])
connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"],
rf.build_rule("GP"))
connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"],
lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
entrance = connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"],
lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
# Access to "DDD: Board Bowser's Sub" does not require access to other locations or regions, so the only region that
# needs to be registered is its parent region.
world.register_indirect_condition(world.get_location("DDD: Board Bowser's Sub", player).parent_region, entrance)
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
@@ -1,6 +1,6 @@
from ..game_content import ContentPack
from ...data import villagers_data, fish_data
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
@@ -229,8 +229,10 @@ pelican_town = ContentPack(
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
Book.mapping_cave_systems: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
CompoundSource(sources=(
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
))),
Book.monster_compendium: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
+5
View File
@@ -59,6 +59,11 @@ class CustomRuleSource(ItemSource):
create_rule: Callable[[Any], StardewRule]
@dataclass(frozen=True, **kw_only)
class CompoundSource(ItemSource):
sources: Tuple[ItemSource, ...] = ()
class Tag(ItemSource):
"""Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking."""
tag: Tuple[ItemTag, ...]
+10 -2
View File
@@ -12,7 +12,7 @@ from .region_logic import RegionLogicMixin
from .requirement_logic import RequirementLogicMixin
from .tool_logic import ToolLogicMixin
from ..data.artisan import MachineSource
from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource
from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource, CompoundSource
from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
@@ -25,7 +25,7 @@ class SourceLogicMixin(BaseLogicMixin):
class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin,
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
def has_access_to_item(self, item: GameItem):
rules = []
@@ -40,6 +40,10 @@ ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
for source in sources))
def has_access_to_all(self, sources: Iterable[ItemSource]):
return self.logic.and_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
for source in sources))
@functools.singledispatchmethod
def has_access_to(self, source: Any):
raise ValueError(f"Sources of type{type(source)} have no rule registered.")
@@ -52,6 +56,10 @@ ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
def _(self, source: CustomRuleSource):
return source.create_rule(self.logic)
@has_access_to.register
def _(self, source: CompoundSource):
return self.logic.source.has_access_to_all(source.sources)
@has_access_to.register
def _(self, source: ForagingSource):
return self.logic.harvesting.can_forage_from(source)
-14
View File
@@ -57,8 +57,6 @@ all_random_settings = {
}
easy_settings = {
"progression_balancing": ProgressionBalancing.default,
"accessibility": Accessibility.option_full,
Goal.internal_name: Goal.option_community_center,
FarmType.internal_name: "random",
StartingMoney.internal_name: "very rich",
@@ -103,8 +101,6 @@ easy_settings = {
}
medium_settings = {
"progression_balancing": 25,
"accessibility": Accessibility.option_full,
Goal.internal_name: Goal.option_community_center,
FarmType.internal_name: "random",
StartingMoney.internal_name: "rich",
@@ -149,8 +145,6 @@ medium_settings = {
}
hard_settings = {
"progression_balancing": 0,
"accessibility": Accessibility.option_full,
Goal.internal_name: Goal.option_grandpa_evaluation,
FarmType.internal_name: "random",
StartingMoney.internal_name: "extra",
@@ -195,8 +189,6 @@ hard_settings = {
}
nightmare_settings = {
"progression_balancing": 0,
"accessibility": Accessibility.option_full,
Goal.internal_name: Goal.option_community_center,
FarmType.internal_name: "random",
StartingMoney.internal_name: "vanilla",
@@ -241,8 +233,6 @@ nightmare_settings = {
}
short_settings = {
"progression_balancing": ProgressionBalancing.default,
"accessibility": Accessibility.option_full,
Goal.internal_name: Goal.option_bottom_of_the_mines,
FarmType.internal_name: "random",
StartingMoney.internal_name: "filthy rich",
@@ -287,8 +277,6 @@ short_settings = {
}
minsanity_settings = {
"progression_balancing": ProgressionBalancing.default,
"accessibility": Accessibility.option_minimal,
Goal.internal_name: Goal.default,
FarmType.internal_name: "random",
StartingMoney.internal_name: StartingMoney.default,
@@ -333,8 +321,6 @@ minsanity_settings = {
}
allsanity_settings = {
"progression_balancing": ProgressionBalancing.default,
"accessibility": Accessibility.option_full,
Goal.internal_name: Goal.default,
FarmType.internal_name: "random",
StartingMoney.internal_name: StartingMoney.default,
+2
View File
@@ -39,6 +39,7 @@ from .strings.crop_names import Fruit, Vegetable
from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \
SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance
from .strings.forageable_names import Forageable
from .strings.generic_names import Generic
from .strings.geode_names import Geode
from .strings.material_names import Material
from .strings.metal_names import MetalBar, Mineral
@@ -263,6 +264,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2))
set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two)
set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three)
set_entrance_rule(multiworld, player, Entrance.adventurer_guild_to_bedroom, logic.monster.can_kill_max(Generic.any))
def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions):
+2 -2
View File
@@ -256,10 +256,10 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
return False
return super().run_default_tests
def collect_lots_of_money(self):
def collect_lots_of_money(self, percent: float = 0.25):
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
required_prog_items = int(round(real_total_prog_items * 0.25))
required_prog_items = int(round(real_total_prog_items * percent))
for i in range(required_prog_items):
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
@@ -0,0 +1,26 @@
from ... import options
from ...test import SVTestBase
class TestBooksLogic(SVTestBase):
options = {
options.Booksanity.internal_name: options.Booksanity.option_all,
}
def test_need_weapon_for_mapping_cave_systems(self):
self.collect_lots_of_money(0.5)
location = self.multiworld.get_location("Read Mapping Cave Systems", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.assert_reach_location_false(location, self.multiworld.state)
self.collect("Progressive Weapon")
self.assert_reach_location_true(location, self.multiworld.state)

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