Compare commits

...

72 Commits

Author SHA1 Message Date
NewSoupVi
eaf352daaf MultiServer: Correct tying of Context.groups 2025-01-11 22:01:05 +01:00
Alchav
29b34ca9fd Pokémon R/B: Fix Route 11-E to Route-12-W logic (#4435) 2025-01-11 01:31:29 +01:00
Fabian Dill
d97ee5d209 Core: update certifi (#4453) 2025-01-10 23:28:57 +01:00
Fabian Dill
c2bd9df0f7 Subnautica: fix typo and remove no longer used logger (#4456) 2025-01-10 23:28:38 +01:00
Scipio Wright
112bfe0933 TUNIC: Logic for Beneath the Vault Bridge Switch #4432 2025-01-10 22:48:15 +01:00
Alchav
96b500679d LTTP: Add missing GT Pre-Moldorm Bomb Wall Logic (#4440) 2025-01-10 22:40:50 +01:00
Scipio Wright
258ea10c52 TUNIC: Modify UT support to make a better pattern (#3860)
* Modify UT support to make a better pattern

* Handle keyerror for logic_rules option

* Missed self.passthrough value setting

* Less laziness for passthrough

* Remove extra newline

* Fix missing using_ut = True, also remove now unnecessary try except since 0.5.1 is out

* New UT thing, it goes in this PR because it's been open for 5 months for a very very tiny change
2025-01-10 21:49:13 +01:00
lordlou
043ba418ec SM generate without rom (#3460)
* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* SM Varia can now generate without ROM

* removed stage_assert_generate
2025-01-10 21:46:17 +01:00
Fabian Dill
894a8571ee kvui: add autocompleting new hint text input (#3535)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2025-01-10 20:21:02 +01:00
ruby0b
874197d940 Linux: move the user home Archipelago dir to $XDG_DATA_HOME (#4347)
This affects builds with non-writable installation directories.
Instead of saving data in ~/Archipelago we now use $XDG_DATA_HOME/Archipelago
(defaulting to ~/.local/share/Archipelago).
If ~/Archipelago still exists we move it to the new location and link ~/Archipelago to it.

Motivation: This follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/)
to at least some degree and doesn't clutter the user's home directory.
2025-01-10 01:27:49 +01:00
agilbert1412
d3ed40cd4d Stardew Valley: Hide the Mods from the simple options page (#4446) 2025-01-08 08:13:32 +01:00
Aaron Wagener
a29ba4a6c4 The Messenger: reduce strictness of output path check (#4442) 2025-01-07 23:11:26 +01:00
Fabian Dill
fe06fe075e Factorio: add fluid mining technology to logic requirements (#4385) 2025-01-07 23:06:48 +01:00
qwint
de58cb03da Core: Pickle hints by value (#4441) 2025-01-07 22:24:19 +01:00
TheLX5
3204680662 SNIClient: Let clients based on SNIClient monitor packages via on_package method (#3093) 2025-01-07 00:10:23 +01:00
shananas
07e896508c KH2: Doc Updates (#4434) 2025-01-06 14:02:04 -05:00
Scipio Wright
2d3faea713 Core: Include unfilled locations in error when there are not enough locations for progression items (#4285)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-06 09:52:33 -05:00
eudaimonistic
7c89a83d19 Docs: Clarify !alias commands in commands_en.md (#4426)
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-01-06 09:42:18 -05:00
qwint
16f8b41cb9 Core: add docstrings for launcher components (#4148)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-06 09:35:37 -05:00
qwint
7d506990f5 HK: add location counts to option descriptions (#4083) 2025-01-06 09:35:12 -05:00
qwint
aadcb4c903 HK: use rich_text_options_doc to make webhost formatting look better (#4079) 2025-01-06 09:21:44 -05:00
coveleski
daf94fcdb2 Pokemon RB: Fixing misnamed locations (#4404) 2025-01-04 08:27:41 -05:00
Kory Dondzila
1cef659b78 Shivers: Fix spelling error in naming (#4425) 2025-01-04 07:42:34 -05:00
Scipio Wright
25381ef2c2 Core: Make the error for a missing option display the player name (#4430) 2025-01-04 07:29:30 -05:00
Mysteryem
5927926314 Blasphemous: Fix starting_location: random affecting all Blasphemous worlds (#4428)
Option resolution for the `StartingLocation` option (the only
`ChoiceIsRandom` subclass) was writing to the `randomized` attribute on
the class instead of on the instance, meaning that
`self.options.starting_location.randomized` would be `True` for all
Blasphemous players in the multiworld if any one of the players set
their `StartingLocation` option to `"random"`.

This patch fixes the issue by writing to the `randomized` attribute on
the new instance instead of on the class.
2025-01-03 07:03:30 -05:00
CaitSith2
2a11d9fec3 try again to award the starting items post cutscene if needed. (#4408) 2025-01-02 19:45:32 -08:00
Nicholas Saylor
82c44aaa22 FFMQ: Fix encoding issue with Game Page (#4299) 2025-01-02 22:03:07 -05:00
Kory Dondzila
a7b483e4b7 Shivers: Adds ixupi captures priority option (#4403) 2025-01-02 10:12:00 -05:00
Fabian Dill
917335ec54 Core: it's 2025 (#4417) 2025-01-01 02:02:18 +01:00
Mysteryem
6e59ee2926 Zork Grand Inquisitor: Precollect Start with Hotspot Items in deterministic order (#4412) 2024-12-31 09:16:29 -05:00
Mysteryem
3c9270d802 FFMQ: Create itempool in deterministic order (#4413) 2024-12-31 09:02:02 -05:00
Mysteryem
c4bbcf9890 TUNIC: Add relics and abilities to the item pool in deterministic order (#4411) 2024-12-30 23:57:09 -05:00
NewSoupVi
8dbecf3d57 The Witness: Make location order in the spoiler log deterministic (#3895)
* Fix location order

* Update worlds/witness/data/static_logic.py

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-12-30 00:50:39 +01:00
Fabian Dill
0de1369ec5 Factorio: hide hidden vanilla techs in factoriopedia too (#4332) 2024-12-29 11:56:41 -08:00
Fabian Dill
fa95ae4b24 Factorio: require version that fixes a randomizer exploit (#4391) 2024-12-29 11:55:40 -08:00
CaitSith2
2065246186 Factorio: Make it possible to use rocket part in blueprint parameterization. (#4396)
This allows for example, making a blueprint of your rocket silo with requester chests specifying a request for the 2-8 rocket part ingredients needed to build the rocket.
2024-12-29 20:13:34 +01:00
Kory Dondzila
ca1b3df45b Shivers: Follow on PR to cleanup options #4401 2024-12-27 23:38:01 +01:00
Kory Dondzila
3bcc86f539 Shivers: Add events and fix require puzzle hints logic (#4018)
* Adds some events, renames things, fails for many players.

* Adds entrance rules for requires hints.

* Cleanup and add goal item.

* Cleanup.

* Add additional rule.

* Event and regions additions.

* Updates from merge.

* Adds collect behavior option.

* Fix missing generator location.

* Fix whitespace and optimize imports.

* Switch location order back.

* Add name replacement for storage.

* Fix test failure.

* Improve puzzle hints required.

* Add missing locations and cleanup indirect conditions.

* Fix naming.

* PR feedback.

* Missed comment.

* Cleanup imports, use strings for option equivalence, and update option description.

* Fix rule.

* Create rolling buffer goal items and remove goal items and location from default options.

* Cleanup.

* Removes dateutil.

* Fixes Subterranean World information plaque.
2024-12-27 21:07:55 +01:00
BadMagic100
218f28912e Core: Generic Entrance Rando (#2883)
* Initial implementation of Generic ER

* Move ERType to Entrance.Type, fix typing imports

* updates based on testing (read: flailing)

* Updates from feedback

* Various bug fixes in ERCollectionState

* Use deque instead of queue.Queue

* Allow partial entrances in collection state earlier, doc improvements

* Prevent early loops in region graph, improve reusability of ER stage code

* Typos, grammar, PEP8, and style "fixes"

* use RuntimeError instead of bare Exceptions

* return tuples from connect since it's slightly faster for our purposes

* move the shuffle to the beginning of find_pairing

* do er_state placements within pairing lookups to remove code duplication

* requested adjustments

* Add some temporary performance logging

* Use CollectionState to track available exits and placed regions

* Add a method to automatically disconnect entrances in a coupled-compliant way

 Update docs and cleanup todos

* Make find_placeable_exits deterministic by sorting blocked_connections set

* Move EntranceType out of Entrance

* Handle minimal accessibility, autodetect regions, and improvements to disconnect

* Add on_connect callback to react to succeeded entrance placements

* Relax island-prevention constraints after a successful run on minimal accessibility; better error message on failure

* First set of unit tests for generic ER

* Change on_connect to send lists, add unit tests for EntranceLookup

* Fix duplicated location names in tests

* Update tests after merge

* Address review feedback, start docs with diagrams

* Fix rendering of hidden nodes in ER doc

* Move most docstring content into a docs article

* Clarify when randomize_entrances can be called safely

* Address review feedback

* Apply suggestions from code review

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

* Docs on ERPlacementState, add coupled/uncoupled handling to deadend detection

* Documentation clarifications

* Update groups to allow any hashable

* Restrict groups from hashable to int

* Implement speculative sweeping in stage 1, address misc review comments

* Clean unused imports in BaseClasses.py

* Restrictive region/speculative sweep test

* sweep_for_events->advancement

* Remove redundant __str__

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

* Allow partial entrances in auto indirect condition sweep

* Treat regions needed for logic as non-dead-end regardless of if they have exits, flip order of stage 3 and 4 to ensure there are enough exits for the dead ends

* Typing fixes suggested by mypy

* Remove erroneous newline 

Not sure why the merge conflict editor is different and worse than the normal editor. Crazy

* Use modern typing for ER

* Enforce the use of explicit indirect conditions

* Improve doc on required indirect conditions

---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-12-27 21:04:02 +01:00
Exempt-Medic
b9642a482f KH2: Using fast_fill instead of fill_restrictive (#4227) 2024-12-26 17:04:21 -05:00
Mysteryem
33ae68c756 DS3: Convert post_fill to stage_post_fill for better performance (#4122)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-12-26 08:50:18 -05:00
NewSoupVi
62942704bd The Witness: Add info about which door items exist in the pool to slot data (#3583)
* This feature is just broken lol

* simplify

* mypy

* Expand the unit test for forbidden doors
2024-12-25 21:55:15 +01:00
NewSoupVi
fe81053521 Core: Give the option to worlds to have a remaining fill that respects excluded locations (#3738)
* Give the option to worlds to have a remaining fill that respects excluded

* comment
2024-12-25 21:53:05 +01:00
NewSoupVi
222c8aa0ae Core: Reword item classification definitions to allow for progression + useful (#3925)
* Core: Reword item classification definitions to allow for progression + useful

* Update network protocol.md

* Update world api.md

* Update Fill.py

* Docstrings

* Update BaseClasses.py

* Update advanced_settings_en.md

* Update advanced_settings_en.md

* Update advanced_settings_en.md

* space
2024-12-25 21:47:51 +01:00
NewSoupVi
845000d10f Docs: Make an actual LogicMixin spec & explanation (#3975)
* Docs: Make an actual LogicMixin spec & explanation

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update world api.md

* Update docs/world api.md

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

* Update docs/world api.md

* Update world api.md

* Code corrections / actually follow own spec

* Update docs/world api.md

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

* Update world api.md

* Update world api.md

* Reorganize / Rewrite the parts about optimisations a bit

* Update world api.md

* Write a big motivation paragraph

* Update world api.md

* Update world api.md

* line break issues

* Update docs/world api.md

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

* Update docs/world api.md

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

* Update docs/world api.md

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

* Update world api.md

* Update docs/world api.md

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-12-25 21:47:17 +01:00
NewSoupVi
b05f81b4b4 The Witness: Fix bridge/elevator items being progression when they shouldn't be #4392 2024-12-25 10:58:27 +01:00
Mysteryem
6c1dc5f645 Landstalker: Fix paths Lantern logic affecting other Landstalker worlds (#4394)
The data from `WORLD_PATHS_JSON` is supposed to be constant logic data
shared by all Landstalker worlds, but `add_path_requirements()` was
modifying this data such that after adding a `Lantern` requirement for a
dark region, subsequent Landstalker worlds to have their logic set could
also be affected by this `Lantern` requirement and previous Landstalker
worlds without damage boosting logic could also be affected by this
`Lantern` requirement because they could all be using the same list
instances. This issue would only occur for paths that have
`"requiredItems"` because all paths without required items would create
a new empty list, avoiding the problem.

The items in `data["itemsPlacedWhenCrossing"]` were also getting added
once for each Landstalker player, but there are no paths that have both
`"itemsPlacedWhenCrossing"` and `"requiredItems"`, so all such cases
would start from a new empty list of required items and avoid modifying
`WORLD_PATHS_JSON`.
2024-12-24 20:44:47 -05:00
Dinopony
5578ccd578 Landstalker: Fix issues on generation (#4345) 2024-12-24 14:08:03 -05:00
Mysteryem
78637c96a7 Tests: Add spheres test for missing indirect conditions (#3924)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-12-24 12:38:46 -05:00
Richard Snider
f3ec82962e Core: Add JSONMessagePart for Hint Status (Hint Priority) (#4387)
* add hint_status JSONMessagePart handling

* add docs for hint_status JSONMessagePart

* fix link ordering

* Rename hint_status type in docs

Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>

* Remove redundant explanation of hint_status field

Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>

* Fix formatting on hint status docs again

Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>

---------

Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>
2024-12-22 19:05:43 +01:00
DrBibop
4f590cdf7b Inscryption: Implement new game (#3621)
* Worked locally before that so this is a lot of work . So, initial push

* Changes in init with better create_regions (Thanks to Phar on discord). Add a rule for victory. Change the regions list to remove menu in the destination.

* Added tests for location rules and changed rule locations to lists instead of sets

* Fixed game var in InscryptionLocation

* Fixed location access by using the same system from The Messenger

* Remove unuse rules in init and add region rules. Add all the act 2 locations and items.

* Add locations rule for the left of the bridge in act 2

* Added test for bridge requirement and added a dash to locationfor clarity

* Added more act 2 rules and removed completion rule

* Created docs for website, added Salmon Card item, marked multiple items as "progression", renamed tomb checks, added more location rules, re-added completion rule

* Renamed tower bath check to "Tentacle", added monocle as requirement for some checks, adjusted setup doc a bit

* Added tentacle to monocle test

* Added forest burrow chest rule

* Switch the two clock location because the id was swapped and screwed with the logic

* Added Ancient Obol rule and adjusted docs

* Added act 3 locations/items/rules/tests

* Added drone & battery to trader rules

* Fixed tutorial docs, added more act 3 rules, renamed holo pelt locations

* Add an option for the optional death card feature

* Added well check and quill item, added rules and tests

* Renamed Gems module and Gems drone

* Added slot data options

* Added rule for act 3 middle pelt

* Added option for randomize ability and uptade the randomize deck option to fit the new setup

* Added randomize ability in slot data

* Added more requirements for mycologists boss since it's pretty much an impossible fight early on

* Finished the french translation of the installation guide

* Changed the french title in the guide

* Added goal option and tests associated to it + fixed goal requirement missing quill

* Added goal option to docs and removed references to the now discarded API mod. Fixed some french translations.

* Added ourobot item + renamed some goal settings

* Fixed locations and items for act 1 goal

* Added skip tutorial option. Cleanup and rename of some options. Added tower requirement for Mycologist Key check. Fixed missing comma in act 2 locations oopsies.

* Added missing rules for Extra Battery, Nano Armor and Goobert's painting

* Added act 1 deathlink behaviour and epitaph pieces randomization options + made pieces progressive + adjusted docs

* Fixed some docs typos

* Added act 3 clock rule. Paintings 2, 3 and Goobert's painting can no longer contain progression items.

* New options system and fixed act 1 goal option breaking

* Added skip epilogue and painting checks balancing options. Renamed randomize abilities to randomize sigils. Fixed generation issue with epitaph pieces randomization. Goobert's painting no longer forces filler. Removed traps option for now. Reworded some option descriptions.

* Attempting type fix for python 3.8

* Attempting type fix for python 3.8 again

* Added starting only option for randomize deck

* Fixed arbitrary rule error

* Import fix attempt

* Migrated to DeathLinkMixin instead of creating a custom DeathLink option, cleaned up imports, renamed Death Link related options to include "death_link" instead of "deathlink", replaced numeral values for option checking into class attributes for readability, slight optimization to tower rule, fixed typo in codes option description.

* Added bug report page to web class, condensed pelt rules to one function, added items/locations count in game docs and adjusted some sections

* Added Inscryption to CODEOWNERS

* Implemented a bunch of suggestions: Better handling of painting option, options as dict for slot data, remove redundant auto_display_name, use of has_all, better goal tests, demote skink card to filler if goal is act 1 and force filler on paintings

* Makes clover plant and squirrel head progression items if paintings are balanced + fixed other issues

* filler items, start inventory from pool, '->"

* Fix bleeding issue

* Copy the list instead

* Fixed bleeding using proper deep copy

* Remove unnecessary for loops in tests

* Add defaults to choice options

---------

Co-authored-by: Benjamin Gregoire <benjamingregoire@outlook.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-12-21 23:12:35 +01:00
Kaito Sinclaire
46613adceb SMZ3: Fix minimal logic considering SM boss tokens unnecessary (#4377) 2024-12-21 20:39:38 +01:00
threeandthreee
e1a1cd1067 LADX: Open Mabe Option (#3964)
* open mabe option
swaps east mabe rocks for bushes

* add open mabe to slot data

* use upstream overworld option
Instead of a standalone option, use upstream's "overworld" option, which we don't use yet but it leaves better space for the future

* use ladxr_setting for consistency

* newline
2024-12-20 07:55:32 -05:00
Scipio Wright
7c8d102c17 TUNIC: Logic for bushes in guard house 2 upper and belltower (#4371)
* Logic for bushes in guard house 2 upper

* Fix typo

* also do it for forest belltower

* i love the dumb ice grapples
2024-12-19 23:45:29 -05:00
threeandthreee
35d30442f7 LADX: fix for syntax warning (#4376)
* init

* whitespace

* raw string instead
2024-12-19 22:53:58 -05:00
threeandthreee
4f71073d17 LADX: correct in-game check counter
LADX: correct in-game check counter
2024-12-19 22:17:41 -05:00
threeandthreee
e142283e64 LADX: enable upstream options (#3962)
* enable some upstream settings

* flashing just disabled, no setting

* just enable fast text

* noflash and textmode as hidden options

* typo

* drop whitespace changes

* add hard mode to slot data

* textmode adjustments
fast text default (fixing mistake)
remove no text option (its buggy)

* unhide options

* Update worlds/ladx/Options.py

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

* adjustments
2024-12-19 21:19:00 -05:00
palex00
de3707af4a Core/Docs: Adding apostrophe quotes around variables in printed error messages (#3914)
* Also indents plando_connections properly

* Adding apostrophe quotes around item, location, entrance/exit and boss names to make errors more readable

* Update plando_en.md

* Fixing test in Lufia II
2024-12-19 20:47:33 -05:00
Scipio Wright
2e0769c90e Noita: Make greed die a trap (#4382)
Noita make greed die a trap
2024-12-19 20:30:41 -05:00
Louis M
1ded7b2fd4 Aquaria: Replacing the release link to the latest link (#4381)
* Replacing the release link to the latest link

* The fr link was not working
2024-12-19 20:17:56 -05:00
Bryce Wilson
cacab68b77 Pokemon Emerald: Remove unnecessary code (#4364) 2024-12-16 09:06:48 +01:00
NewSoupVi
728d249202 Core: Add some more world convenience methods (#3021)
* Add some more convenience methods

* Typing stuff

* Rename the method

* beauxq's suggestions

* Back to Push Precollected
2024-12-15 23:30:35 +01:00
qwint
d1823a21ea HK: add random handling to plandocharmcosts (#4327) 2024-12-15 22:48:44 +01:00
Scipio Wright
6282efb13c TUNIC: Additional Combat Logic Option (#3658) 2024-12-15 22:40:36 +01:00
Benjamin S Wolf
0fdc14bc42 Core: Deduplicate exception output (#4036)
When running Generate.py, uncaught exceptions are logged once to a file and twice to the console due to keeping the original excepthook. We can avoid this by filtering the file log out of the stream handler.
2024-12-15 22:29:56 +01:00
Mysteryem
0370e669e5 Pokemon Emerald: Add Mr Briney's House indirect conditions (#4154)
The `REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH` and
`REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN` entrances require
access to the
`REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN`
entrance in their access rules, so require indirect conditions for the
parent_region of the entrance: `REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN`.
2024-12-15 22:28:51 +01:00
threeandthreee
ccea6bcf51 LADX: Improve icon guesses for foreign items (#2201)
* synonyms to new file, many added

* handle singular rupee

* remove redundant map and compass entries

* automatic pluralization

* add guardian acorn and piece of power

* move phrases to ItemIconGuessing.py

* organize, comment

* fix tab spacing

* fix

* add tunic and noita synonyms

* remove triangle instrument synonym

* reorganize, add some matches

* add tunic lucky up

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

* Update worlds/ladx/ItemIconGuessing.py

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

* handle camelCase and single rupee

* add indicate_progression option
Adds alternative system for foreign item icons that simply indicates whether or not the item is a progression item.

* improve splitting
drops some more characters, and also dont bother with rejoined stuff in name_cache because our splitting is better

* the witness stuff

* forbid more

* remove boost and surge

* Update worlds/ladx/ItemIconGuessing.py

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

* match by game name
look at the name of the foreign game and only use game-specific entries for that game

* show message for all key drops

* updates from async test

* vi suggestions

* Adding FNAFW suggestions from @lolz1190 (#40)

* Adding FNAFW suggestions from @lolz1190

* missing comma

---------

Co-authored-by: threeandthreee <a.l.nordstrom@gmail.com>

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com>
2024-12-13 22:49:30 +01:00
qwint
8d9454ea3b Core: cast all the settings values so they don't try to get pickled later #4362 2024-12-12 21:36:56 +01:00
qwint
1ca8d3e4a8 Docs: add description of Indirect Condition problem (#4295)
* Docs: Dev FAQ - About indirect conditions

I wrote up a big effortpost about indirect conditions for nex on the [DS3 3.0 PR](https://github.com/ArchipelagoMW/Archipelago/pull/3128#discussion_r1693843193).

The version I'm [PRing to the world API document](https://github.com/ArchipelagoMW/Archipelago/pull/3552) is very brief and unnuanced, because I'd rather people use too many indirect conditions than too few.
But that might leave some devs wanting to know more.

I think that comment on nex's DS3 PR is probably the best detailed explanation for indirect conditions that exists currently.

So I think it's good if it exists somewhere. And the FAQ doc seems like the best place right now, because I don't want to write an entirely new doc at the moment.

* Actually copy in the text

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Update docs/apworld_dev_faq.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Update apworld_dev_faq.md

* Update docs/apworld_dev_faq.md

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

* Update apworld_dev_faq.md

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

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

* Update apworld_dev_faq.md

* Update docs/apworld_dev_faq.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Update docs/apworld_dev_faq.md

Co-authored-by: qwint <qwint.42@gmail.com>

* fix the last couple of wording issues I have with the indirect condition section to apworld dev faq doc

* I didn't like that wording

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Update docs/apworld_dev_faq.md

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

* Update docs/apworld_dev_faq.md

* Update docs/apworld_dev_faq.md

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

---------

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: Scipio Wright <scipiowright@gmail.com>
2024-12-12 21:24:38 +01:00
qwint
9815306875 Docs: Use ModuleUpdate.py #3785 2024-12-12 20:30:49 +01:00
NewSoupVi
d7736950cd The Witness: Panel Hunt Plando (#3549)
* Add panel hunt plando option

* Keys are strs

* oops

* better message

* ,

* this doesn ot need to be here

* don't replace pre picked panels

* Update options.py

* rebase error

* rebase error

* oops

* Mypy

* ruff

* another rebase error

* actually this is a stupid change too

* bring over that change™️

* Update entity_hunt.py

* Update entity_hunt.py

* Update entity_hunt.py
2024-12-12 19:42:14 +01:00
Mysteryem
f5e3677ef1 Pokemon Emerald: Fix invalid escape sequence warnings (#4328)
Generation on Python 3.12 would print SyntaxWarnings due to invalid '\d'
escape sequences added in #3832.

Use raw strings to avoid `\` being used to escape characters.
2024-12-12 19:04:27 +01:00
130 changed files with 6580 additions and 1259 deletions

View File

@@ -19,6 +19,7 @@ import Options
import Utils
if TYPE_CHECKING:
from entrance_rando import ERPlacementState
from worlds import AutoWorld
@@ -426,12 +427,12 @@ class MultiWorld():
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]
def get_all_state(self, use_cache: bool) -> CollectionState:
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()
ret = CollectionState(self)
ret = CollectionState(self, allow_partial_entrances)
for item in self.itempool:
self.worlds[item.player].collect(ret, item)
@@ -717,10 +718,11 @@ class CollectionState():
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []
def __init__(self, parent: MultiWorld):
def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False):
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
@@ -729,6 +731,7 @@ class CollectionState():
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
self.allow_partial_entrances = allow_partial_entrances
for function in self.additional_init_functions:
function(self, parent)
for items in parent.precollected_items.values():
@@ -763,6 +766,8 @@ class CollectionState():
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
@@ -788,7 +793,9 @@ class CollectionState():
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
@@ -808,6 +815,7 @@ class CollectionState():
ret.advancements = self.advancements.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
ret.allow_partial_entrances = self.allow_partial_entrances
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
@@ -972,6 +980,11 @@ class CollectionState():
self.stale[item.player] = True
class EntranceType(IntEnum):
ONE_WAY = 1
TWO_WAY = 2
class Entrance:
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
hide_path: bool = False
@@ -979,19 +992,24 @@ class Entrance:
name: str
parent_region: Optional[Region]
connected_region: Optional[Region] = None
randomization_group: int
randomization_type: EntranceType
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
self.name = name
self.parent_region = parent
self.player = player
self.randomization_group = randomization_group
self.randomization_type = randomization_type
def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
if not self.hide_path and self not in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True
@@ -1003,6 +1021,32 @@ class Entrance:
self.addresses = addresses
region.entrances.append(self)
def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
"""
Determines whether this is a valid source transition, that is, whether the entrance
randomizer is allowed to pair it to place any other regions. By default, this is the
same as a reachability check, but can be modified by Entrance implementations to add
other restrictions based on the placement state.
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
return self.can_reach(er_state.collection_state)
def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool:
"""
Determines whether a given Entrance is a valid target transition, that is, whether
the entrance randomizer is allowed to pair this Entrance to that Entrance. By default,
only allows connection between entrances of the same type (one ways only go to one ways,
two ways always go to two ways) and prevents connecting an exit to itself in coupled mode.
:param other: The proposed Entrance to connect to
:param dead_end: Whether the other entrance considered a dead end by Entrance randomization
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
# the implementation of coupled causes issues for self-loops since the reverse entrance will be the
# same as the forward entrance. In uncoupled they are ok.
return self.randomization_type == other.randomization_type and (not er_state.coupled or self.name != other.name)
def __repr__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1152,6 +1196,16 @@ class Region:
self.exits.append(exit_)
return exit_
def create_er_target(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an entrance to this region
:param name: name of the Entrance being created
"""
entrance = self.entrance_type(self.player, name)
entrance.connect(self)
return entrance
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
@@ -1254,13 +1308,26 @@ class Location:
class ItemClassification(IntFlag):
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.
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. """
progression_skip_balancing = 0b1001 # only progression gets balanced
def as_flag(self) -> int:

23
Fill.py
View File

@@ -235,18 +235,30 @@ def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item],
name: str = "Remaining",
move_unplaceable_to_start_inventory: bool = False) -> None:
move_unplaceable_to_start_inventory: bool = False,
check_location_can_fill: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
total = min(len(itempool), len(locations))
placed = 0
# Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule
if check_location_can_fill:
state = CollectionState(multiworld)
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
return location_to_fill.can_fill(state, item_to_fill, check_access=False)
else:
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
return location_to_fill.item_rule(item_to_fill)
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
for i, location in enumerate(locations):
if location.item_rule(item_to_place):
if location_can_fill_item(location, item_to_place):
# popping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
@@ -267,7 +279,7 @@ def remaining_fill(multiworld: MultiWorld,
location.item = None
placed_item.location = None
if location.item_rule(item_to_place):
if location_can_fill_item(location, item_to_place):
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
@@ -519,7 +531,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations.",
f"There are {len(progitempool)} more progression items than there are available locations.\n"
f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.",
multiworld=multiworld,
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
@@ -537,7 +550,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 filler or trap items.",
f"There are {len(excludedlocations)} more excluded locations than excludable items.",
multiworld=multiworld,
)

View File

@@ -500,7 +500,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key in game_weights:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":

View File

@@ -1,7 +1,7 @@
MIT License
Copyright (c) 2017 LLCoolDave
Copyright (c) 2022 Berserker66
Copyright (c) 2025 Berserker66
Copyright (c) 2022 CaitSith2
Copyright (c) 2021 LegendaryLinux

View File

@@ -235,7 +235,7 @@ class RAGameboy():
def check_command_response(self, command: str, response: bytes):
if command == "VERSION":
ok = re.match("\d+\.\d+\.\d+", response.decode('ascii')) is not None
ok = re.match(r"\d+\.\d+\.\d+", response.decode('ascii')) is not None
else:
ok = response.startswith(command.encode())
if not ok:

View File

@@ -444,7 +444,7 @@ class Context:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
self.clients = {0: {}}

View File

@@ -10,6 +10,14 @@ import websockets
from Utils import ByValue, Version
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_NO_PRIORITY = 10
HINT_AVOID = 20
HINT_PRIORITY = 30
class JSONMessagePart(typing.TypedDict, total=False):
text: str
# optional
@@ -19,6 +27,8 @@ class JSONMessagePart(typing.TypedDict, total=False):
player: int
# if type == item indicates item flags
flags: int
# if type == hint_status
hint_status: HintStatus
class ClientStatus(ByValue, enum.IntEnum):
@@ -29,14 +39,6 @@ class ClientStatus(ByValue, enum.IntEnum):
CLIENT_GOAL = 30
class HintStatus(enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_NO_PRIORITY = 10
HINT_AVOID = 20
HINT_PRIORITY = 30
class SlotType(ByValue, enum.IntFlag):
spectator = 0b00
player = 0b01
@@ -192,6 +194,7 @@ class JSONTypes(str, enum.Enum):
location_name = "location_name"
location_id = "location_id"
entrance_name = "entrance_name"
hint_status = "hint_status"
class JSONtoTextParser(metaclass=HandlerMeta):
@@ -273,6 +276,10 @@ class JSONtoTextParser(metaclass=HandlerMeta):
node["color"] = 'blue'
return self._handle_color(node)
def _handle_hint_status(self, node: JSONMessagePart):
node["color"] = status_colors.get(node["hint_status"], "red")
return self._handle_color(node)
class RawJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
@@ -319,6 +326,13 @@ status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum",
}
def add_json_hint_status(parts: list, hint_status: HintStatus, text: typing.Optional[str] = None, **kwargs):
parts.append({"text": text if text != None else status_names.get(hint_status, "(unknown)"),
"hint_status": hint_status, "type": JSONTypes.hint_status, **kwargs})
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
@@ -363,8 +377,7 @@ class Hint(typing.NamedTuple):
else:
add_json_text(parts, "'s World")
add_json_text(parts, ". ")
add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color",
color=status_colors.get(self.status, "red"))
add_json_hint_status(parts, self.status)
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,

View File

@@ -496,7 +496,7 @@ class TextChoice(Choice):
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
f"'{value}' is not a valid option for '{self.__class__.__name__}'"
self.value = value
@property
@@ -617,17 +617,17 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
used_locations.append(location)
used_bosses.append(boss)
if not cls.valid_boss_name(boss):
raise ValueError(f"{boss.title()} is not a valid boss name.")
raise ValueError(f"'{boss.title()}' is not a valid boss name.")
if not cls.valid_location_name(location):
raise ValueError(f"{location.title()} is not a valid boss location name.")
raise ValueError(f"'{location.title()}' is not a valid boss location name.")
if not cls.can_place_boss(boss, location):
raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.")
raise ValueError(f"'{location.title()}' is not a valid location for {boss.title()} to be placed.")
else:
if cls.duplicate_bosses:
if not cls.valid_boss_name(option):
raise ValueError(f"{option} is not a valid boss name.")
raise ValueError(f"'{option}' is not a valid boss name.")
else:
raise ValueError(f"{option.title()} is not formatted correctly.")
raise ValueError(f"'{option.title()}' is not formatted correctly.")
@classmethod
def can_place_boss(cls, boss: str, location: str) -> bool:
@@ -817,15 +817,15 @@ class VerifyKeys(metaclass=FreezeValidKeys):
for item_name in self.value:
if item_name not in world.item_names:
picks = get_fuzzy_results(item_name, world.item_names, limit=1)
raise Exception(f"Item {item_name} from option {self} "
f"is not a valid item name from {world.game}. "
raise Exception(f"Item '{item_name}' from option '{self}' "
f"is not a valid item name from '{world.game}'. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
elif self.verify_location_name:
for location_name in self.value:
if location_name not in world.location_names:
picks = get_fuzzy_results(location_name, world.location_names, limit=1)
raise Exception(f"Location {location_name} from option {self} "
f"is not a valid location name from {world.game}. "
raise Exception(f"Location '{location_name}' from option '{self}' "
f"is not a valid location name from '{world.game}'. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
def __iter__(self) -> typing.Iterator[typing.Any]:
@@ -1111,11 +1111,11 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
used_entrances.append(entrance)
used_exits.append(exit)
if not cls.validate_entrance_name(entrance):
raise ValueError(f"{entrance.title()} is not a valid entrance.")
raise ValueError(f"'{entrance.title()}' is not a valid entrance.")
if not cls.validate_exit_name(exit):
raise ValueError(f"{exit.title()} is not a valid exit.")
raise ValueError(f"'{exit.title()}' is not a valid exit.")
if not cls.can_connect(entrance, exit):
raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.")
raise ValueError(f"Connection between '{entrance.title()}' and '{exit.title()}' is invalid.")
@classmethod
def from_any(cls, data: PlandoConFromAnyType) -> Self:
@@ -1379,8 +1379,8 @@ class ItemLinks(OptionList):
picks_group = get_fuzzy_results(item_name, world.item_name_groups.keys(), limit=1)
picks_group = f" or '{picks_group[0][0]}' ({picks_group[0][1]}% sure)" if allow_item_groups else ""
raise Exception(f"Item {item_name} from item link {item_link} "
f"is not a valid item from {world.game} for {pool_name}. "
raise Exception(f"Item '{item_name}' from item link '{item_link}' "
f"is not a valid item from '{world.game}' for '{pool_name}'. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure){picks_group}")
if allow_item_groups:
pool |= world.item_name_groups.get(item_name, {item_name})

View File

@@ -79,6 +79,7 @@ Currently, the following games are supported:
* Faxanadu
* Saving Princess
* Castlevania: Circle of the Moon
* Inscryption
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -243,6 +243,9 @@ class SNIContext(CommonContext):
# Once the games handled by SNIClient gets made to be remote items,
# this will no longer be needed.
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
def run_gui(self) -> None:
from kvui import GameManager

View File

@@ -152,8 +152,15 @@ def home_path(*path: str) -> str:
if hasattr(home_path, 'cached_path'):
pass
elif sys.platform.startswith('linux'):
home_path.cached_path = os.path.expanduser('~/Archipelago')
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
home_path.cached_path = xdg_data_home + '/Archipelago'
if not os.path.isdir(home_path.cached_path):
legacy_home_path = os.path.expanduser('~/Archipelago')
if os.path.isdir(legacy_home_path):
os.renames(legacy_home_path, home_path.cached_path)
os.symlink(home_path.cached_path, legacy_home_path)
else:
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
else:
# not implemented
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
@@ -534,7 +541,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.getLogger(exception_logger).exception("Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback))
exc_info=(exc_type, exc_value, exc_traceback),
extra={"NoStream": exception_logger is None})
return orig_hook(exc_type, exc_value, exc_traceback)
handle_exception._wrapped = True

View File

@@ -31,11 +31,11 @@ def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[s
server_options = {
"hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)),
"release_mode": options_source.get("release_mode", ServerOptions.release_mode),
"remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode),
"collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode),
"release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)),
"remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)),
"collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)),
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
"server_password": options_source.get("server_password", None),
"server_password": str(options_source.get("server_password", None)),
}
generator_options = {
"spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)),

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2024 Archipelago</div>
<div id="copyright-notice">Copyright 2025 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-

View File

@@ -147,3 +147,8 @@
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
<ServerToolTip>:
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
<AutocompleteHintInput>
size_hint_y: None
height: dp(30)
multiline: False
write_tab: False

View File

@@ -81,6 +81,9 @@
# Hylics 2
/worlds/hylics2/ @TRPG0
# Inscryption
/worlds/inscryption/ @DrBibop @Glowbuzz
# Kirby's Dream Land 3
/worlds/kdl3/ @Silvris
@@ -149,7 +152,7 @@
/worlds/saving_princess/ @LeonarthCG
# Shivers
/worlds/shivers/ @GodlFire
/worlds/shivers/ @GodlFire @korydondzila
# A Short Hike
/worlds/shorthike/ @chandler05 @BrandenEK

View File

@@ -43,3 +43,26 @@ A faster alternative to the `for` loop would be to use a [list comprehension](ht
```py
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
```
---
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and **when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is quite complicated.
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph. It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to the queue until there is nothing more to check.
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region access, then the following may happen:
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been reached yet during the graph search.
2. Then, the region in its access_rule is determined to be reachable.
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new regions are reached.
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep if a specific region is reached during it.
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness), using them is significantly faster than just "rechecking each entrance until nothing new is found".
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they call `region.can_reach` on their respective parent/source region.
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance &rarr; region dependencies, making indirect conditions preferred because they are much faster.

View File

@@ -0,0 +1,430 @@
# Entrance Randomization
This document discusses the API and underlying implementation of the generic entrance randomization algorithm
exposed in [entrance_rando.py](/entrance_rando.py). Throughout the doc, entrance randomization is frequently abbreviated
as "ER."
This doc assumes familiarity with Archipelago's graph logic model. If you don't have a solid understanding of how
regions work, you should start there.
## Entrance randomization concepts
### Terminology
Some important terminology to understand when reading this doc and working with ER is listed below.
* Entrance rando - sometimes called "room rando," "transition rando," "door rando," or similar,
this is a game mode in which the game map itself is randomized.
In Archipelago, these things are often represented as `Entrance`s in the region graph, so we call it Entrance rando.
* Entrances and exits - entrances are ways into your region, exits are ways out of the region. In code, they are both
represented as `Entrance` objects. In this doc, the terms "entrances" and "exits" will be used in this sense; the
`Entrance` class will always be referenced in a code block with an uppercase E.
* Dead end - a connected group of regions which can never help ER progress. This means that it:
* Is not in any indirect conditions/access rules.
* Has no plando'd or otherwise preplaced progression items, including events.
* Has no randomized exits.
* One way transition - a transition that, in the game, is not safe to reverse through (for example, in Hollow Knight,
some transitions are inaccessible backwards in vanilla and would put you out of bounds). One way transitions are
paired together during randomization to prevent such unsafe game states. Most transitions are not one way.
### Basic randomization strategy
The Generic ER algorithm works by using the logic structures you are already familiar with. To give a basic example,
let's assume a toy world is defined with the vanilla region graph modeled below. In this diagram, the smaller boxes
represent regions while the larger boxes represent scenes. Scenes are not an Archipelago concept, the grouping is
purely illustrative.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Lower Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Upper Left Door] <--> AR1
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> AL2
BR1 <--> AL1
AR1 <--> CL1
CR1 <--> DL1
DR1 <--> EL1
CR2 <--> EL2
classDef hidden display:none;
```
First, the world begins by splitting the `Entrance`s which should be randomized. This is essentially all that has to be
done on the world side; calling the `randomize_entrances` function will do the rest, using your region definitions and
logic to generate a valid world layout by connecting the partially connected edges you've defined. After you have done
that, your region graph might look something like the following diagram. Note how each randomizable entrance/exit pair
(represented as a bidirectional arrow) is disconnected on one end.
> [!NOTE]
> It is required to use explicit indirect conditions when using Generic ER. Without this restriction,
> Generic ER would have no way to correctly determine that a region may be required in logic,
> leading to significantly higher failure rates due to mis-categorized regions.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Lower Left Door] <--> AR1
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> T1:::hidden
T2:::hidden <--> AL1
T3:::hidden <--> AL2
AR1 <--> T5:::hidden
BR1 <--> T4:::hidden
T6:::hidden <--> CL1
CR1 <--> T7:::hidden
CR2 <--> T11:::hidden
T8:::hidden <--> DL1
DR1 <--> T9:::hidden
T10:::hidden <--> EL1
T12:::hidden <--> EL2
classDef hidden display:none;
```
From here, you can call the `randomize_entrances` function and Archipelago takes over. Starting from the Menu region,
the algorithm will sweep out to find eligible region exits to randomize. It will then select an eligible target entrance
and connect them, prioritizing giving access to unvisited regions first until all regions are placed. Once the exit has
been connected to the new region, placeholder entrances are deleted. This process is visualized in the diagram below
with the newly connected edge highlighted in red.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Lower Left Door] <--> AR1
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> CL1
T2:::hidden <--> AL1
T3:::hidden <--> AL2
AR1 <--> T5:::hidden
BR1 <--> T4:::hidden
CR1 <--> T7:::hidden
CR2 <--> T11:::hidden
T8:::hidden <--> DL1
DR1 <--> T9:::hidden
T10:::hidden <--> EL1
T12:::hidden <--> EL2
classDef hidden display:none;
linkStyle 8 stroke:red,stroke-width:5px;
```
This process is then repeated until all disconnected `Entrance`s have been connected or deleted, eventually resulting
in a randomized region layout.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Lower Left Door] <--> AR1
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> CL1
AR1 <--> DL1
BR1 <--> EL2
CR1 <--> EL1
CR2 <--> AL1
DR1 <--> AL2
classDef hidden display:none;
```
#### ER and minimal accessibility
In general, even on minimal accessibility, ER will prefer to provide access to as many regions as possible. This is for
2 reasons:
1. Generally, having items spread across the world is going to be a more fun/engaging experience for players than
severely restricting their map. Imagine an ER arrangement with just the start region, the goal region, and exactly
enough locations in between them to get the goal - this may be the intuitive behavior of minimal, or even the desired
behavior in some cases, but it is not a particularly interesting randomizer.
2. Giving access to more of the world will give item fill a higher chance to succeed.
However, ER will cull unreachable regions and exits if necessary to save the generation of a beaten minimal.
## Usage
### Defining entrances to be randomized
The first step to using generic ER is defining entrances to be randomized. In order to do this, you will need to
leave partially disconnected exits without a `target_region` and partially disconnected entrances without a
`parent_region`. You can do this either by hand using `region.create_exit` and `region.create_er_target`, or you can
create your vanilla region graph and then use `disconnect_entrance_for_randomization` to split the desired edges.
If you're not sure which to use, prefer the latter approach as it will automatically satisfy the requirements for
coupled randomization (discussed in more depth later).
> [!TIP]
> It's recommended to give your `Entrance`s non-default names when creating them. The default naming scheme is
> `f"{parent_region} -> {target_region}"` which is generally not helpful in an entrance rando context - after all,
> the target region will not be the same as vanilla and regions are often not user-facing anyway. Instead consider names
> that describe the location of the exit, such as "Starting Room Right Door."
When creating your `Entrance`s you should also set the randomization type and group. One-way `Entrance`s represent
transitions which are impossible to traverse in reverse. All other transitions are two-ways. To ensure that all
transitions can be accessed in the game, one-ways are only randomized with other one-ways and two-ways are only
randomized with other two-ways. You can set whether an `Entrance` is one-way or two-way using the `randomization_type`
attribute.
`Entrance`s can also set the `randomization_group` attribute to allow for grouping during randomization. This can be
any integer you define and may be based on player options. Some possible use cases for grouping include:
* Directional matching - only match leftward-facing transitions to rightward-facing ones
* Terrain matching - only match water transitions to water transitions and land transitions to land transitions
* Dungeon shuffle - only shuffle entrances within a dungeon/area with each other
* Combinations of the above
By default, all `Entrance`s are placed in the group 0. An entrance can only be a member of one group, but a given group
may connect to many other groups.
### Calling generic ER
Once you have defined all your entrances and exits and connected the Menu region to your region graph, you can call
`randomize_entrances` to perform randomization.
#### Coupled and uncoupled modes
In coupled randomization, an entrance placed from A to B guarantees that the reverse placement B to A also exists
(assuming that A and B are both two-way doors). Uncoupled randomization does not make this guarantee.
When using coupled mode, there are some requirements for how placeholder ER targets for two-ways are named.
`disconnect_entrance_for_randomization` will handle this for you. However, if you opt to create your ER targets and
exits by hand, you will need to ensure that ER targets into a region are named the same as the exit they correspond to.
This allows the randomizer to find and connect the reverse pairing after the first pairing is completed. See the diagram
below for an example of incorrect and correct naming.
Incorrect target naming:
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph a [" "]
direction TB
target1
target2
end
subgraph b [" "]
direction TB
Region
end
Region["Room1"] -->|Room1 Right Door| target1:::hidden
Region --- target2:::hidden -->|Room2 Left Door| Region
linkStyle 1 stroke:none;
classDef hidden display:none;
style a display:none;
style b display:none;
```
Correct target naming:
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph a [" "]
direction TB
target1
target2
end
subgraph b [" "]
direction TB
Region
end
Region["Room1"] -->|Room1 Right Door| target1:::hidden
Region --- target2:::hidden -->|Room1 Right Door| Region
linkStyle 1 stroke:none;
classDef hidden display:none;
style a display:none;
style b display:none;
```
#### Implementing grouping
When you created your entrances, you defined the group each entrance belongs to. Now you will have to define how groups
should connect with each other. This is done with the `target_group_lookup` and `preserve_group_order` parameters.
There is also a convenience function `bake_target_group_lookup` which can help to prepare group lookups when more
complex group mapping logic is needed. Some recipes for `target_group_lookup` are presented here.
For the recipes below, assume the following groups (if the syntax used here is unfamiliar to you, "bit masking" and
"bitwise operators" would be the terms to search for):
```python
class Groups(IntEnum):
# Directions
LEFT = 1
RIGHT = 2
TOP = 3
BOTTOM = 4
DOOR = 5
# Areas
FIELD = 1 << 3
CAVE = 2 << 3
MOUNTAIN = 3 << 3
# Bitmasks
DIRECTION_MASK = FIELD - 1
AREA_MASK = ~0 << 3
```
Directional matching:
```python
direction_matching_group_lookup = {
# with preserve_group_order = False, pair a left transition to either a right transition or door randomly
# with preserve_group_order = True, pair a left transition to a right transition, or else a door if no
# viable right transitions remain
Groups.LEFT: [Groups.RIGHT, Groups.DOOR],
# ...
}
```
Terrain matching or dungeon shuffle:
```python
def randomize_within_same_group(group: int) -> List[int]:
return [group]
identity_group_lookup = bake_target_group_lookup(world, randomize_within_same_group)
```
Directional + area shuffle:
```python
def get_target_groups(group: int) -> List[int]:
# example group: LEFT | CAVE
# example result: [RIGHT | CAVE, DOOR | CAVE]
direction = group & Groups.DIRECTION_MASK
area = group & Groups.AREA_MASK
return [pair_direction | area for pair_direction in direction_matching_group_lookup[direction]]
target_group_lookup = bake_target_group_lookup(world, get_target_groups)
```
#### When to call `randomize_entrances`
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
This means 2 things about when you can call ER:
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
and create your events before you call ER if you want to guarantee a correct output.
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
well.
#### Informing your client about randomized entrances
`randomize_entrances` returns the completed `ERPlacementState`. The `pairings` attribute contains a list of the
created placements by name which can be used to populate slot data.
### Imposing custom constraints on randomization
Generic ER is, as the name implies, generic! That means that your world may have some use case which is not covered by
the ER implementation. To solve this, you can create a custom `Entrance` class which provides custom implementations
for `is_valid_source_transition` and `can_connect_to`. These allow arbitrary constraints to be implemented on
randomization, for instance helping to prevent restrictive sphere 1s or ensuring a maximum distance from a "hub" region.
> [!IMPORTANT]
> When implementing these functions, make sure to use `super().is_valid_source_transition` and `super().can_connect_to`
> as part of your implementation. Otherwise ER may behave unexpectedly.
## Implementation details
This section is a medium-level explainer of the implementation of ER for those who don't want to decipher the code.
However, a basic understanding of the mechanics of `fill_restrictive` will be helpful as many of the underlying
algorithms are shared
ER uses a forward fill approach to create the region layout. First, ER collects `all_state` and performs a region sweep
from Menu, similar to fill. ER then proceeds in stages to complete the randomization:
1. Attempt to connect all non-dead-end regions, prioritizing access to unseen regions so there will always be new exits
to pair off.
2. Attempt to connect all dead-end regions, so that all regions will be placed
3. Connect all remaining dangling edges now that all regions are placed.
1. Connect any other dead end entrances (e.g. second entrances to the same dead end regions).
2. Connect all remaining non-dead-ends amongst each other.
The process for each connection will do the following:
1. Select a randomizable exit of a reachable region which is a valid source transition.
2. Get its group and check `target_group_lookup` to determine which groups are valid targets.
3. Look up ER targets from those groups and find one which is valid according to `can_connect_to`
4. Connect the source exit to the target's target_region and delete the target.
* In stage 1, before placing the last valid source transition, an additional speculative sweep is performed to ensure
that there will be an available exit after the placement so randomization can continue.
5. If it's coupled mode, find the reverse exit and target by name and connect them as well.
6. Sweep to update reachable regions.
7. Call the `on_connect` callback.
This process repeats until the stage is complete, no valid source transition is found, or no valid target transition is
found for any source transition. Unlike fill, there is no attempt made to save a failed randomization.

View File

@@ -540,7 +540,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 important but not in a way that unlocks advancement |
| 0b010 | If set, indicates the item is especially useful |
| 0b100 | If set, indicates the item is a trap |
### JSONMessagePart
@@ -554,6 +554,7 @@ class JSONMessagePart(TypedDict):
color: Optional[str] # only available if type is a color
flags: Optional[int] # only available if type is an item_id or item_name
player: Optional[int] # only available if type is either item or location
hint_status: Optional[HintStatus] # only available if type is hint_status
```
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
@@ -569,6 +570,7 @@ Possible values for `type` include:
| location_id | Location ID, should be resolved to Location Name |
| location_name | Location Name, not currently used over network, but supported by reference Clients. |
| entrance_name | Entrance Name. No ID mapping exists. |
| hint_status | The [HintStatus](#HintStatus) of the hint. Both `text` and `hint_status` are given. |
| color | Regular text that should be colored. Only `type` that will contain `color` data. |

View File

@@ -43,9 +43,9 @@ Recommended steps
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
* In PyCharm: right-click Generate.py and select `Run 'Generate'`
* Without PyCharm: open a command prompt in the source folder and type `py Generate.py`
* Run ModuleUpdate.py which will prompt installation of missing modules, press enter to confirm
* In PyCharm: right-click ModuleUpdate.py and select `Run 'ModuleUpdate'`
* Without PyCharm: open a command prompt in the source folder and type `py ModuleUpdate.py`
## macOS

View File

@@ -248,7 +248,8 @@ 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`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
* `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".
* `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)
@@ -699,9 +700,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

447
entrance_rando.py Normal file
View File

@@ -0,0 +1,447 @@
import itertools
import logging
import random
import time
from collections import deque
from collections.abc import Callable, Iterable
from BaseClasses import CollectionState, Entrance, Region, EntranceType
from Options import Accessibility
from worlds.AutoWorld import World
class EntranceRandomizationError(RuntimeError):
pass
class EntranceLookup:
class GroupLookup:
_lookup: dict[int, list[Entrance]]
def __init__(self):
self._lookup = {}
def __len__(self):
return sum(map(len, self._lookup.values()))
def __bool__(self):
return bool(self._lookup)
def __getitem__(self, item: int) -> list[Entrance]:
return self._lookup.get(item, [])
def __iter__(self):
return itertools.chain.from_iterable(self._lookup.values())
def __repr__(self):
return str(self._lookup)
def add(self, entrance: Entrance) -> None:
self._lookup.setdefault(entrance.randomization_group, []).append(entrance)
def remove(self, entrance: Entrance) -> None:
group = self._lookup[entrance.randomization_group]
group.remove(entrance)
if not group:
del self._lookup[entrance.randomization_group]
dead_ends: GroupLookup
others: GroupLookup
_random: random.Random
_expands_graph_cache: dict[Entrance, bool]
_coupled: bool
def __init__(self, rng: random.Random, coupled: bool):
self.dead_ends = EntranceLookup.GroupLookup()
self.others = EntranceLookup.GroupLookup()
self._random = rng
self._expands_graph_cache = {}
self._coupled = coupled
def _can_expand_graph(self, entrance: Entrance) -> bool:
"""
Checks whether an entrance is able to expand the region graph, either by
providing access to randomizable exits or by granting access to items or
regions used in logic conditions.
:param entrance: A randomizable (no parent) region entrance
"""
# we've seen this, return cached result
if entrance in self._expands_graph_cache:
return self._expands_graph_cache[entrance]
visited = set()
q: deque[Region] = deque()
q.append(entrance.connected_region)
while q:
region = q.popleft()
visited.add(region)
# check if the region itself is progression
if region in region.multiworld.indirect_connections:
self._expands_graph_cache[entrance] = True
return True
# check if any placed locations are progression
for loc in region.locations:
if loc.advancement:
self._expands_graph_cache[entrance] = True
return True
# check if there is a randomized exit out (expands the graph directly) or else search any connected
# regions to see if they are/have progression
for exit_ in region.exits:
# randomizable exits which are not reverse of the incoming entrance.
# uncoupled mode is an exception because in this case going back in the door you just came in could
# actually lead somewhere new
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name):
self._expands_graph_cache[entrance] = True
return True
elif exit_.connected_region and exit_.connected_region not in visited:
q.append(exit_.connected_region)
self._expands_graph_cache[entrance] = False
return False
def add(self, entrance: Entrance) -> None:
lookup = self.others if self._can_expand_graph(entrance) else self.dead_ends
lookup.add(entrance)
def remove(self, entrance: Entrance) -> None:
lookup = self.others if self._can_expand_graph(entrance) else self.dead_ends
lookup.remove(entrance)
def get_targets(
self,
groups: Iterable[int],
dead_end: bool,
preserve_group_order: bool
) -> Iterable[Entrance]:
lookup = self.dead_ends if dead_end else self.others
if preserve_group_order:
for group in groups:
self._random.shuffle(lookup[group])
ret = [entrance for group in groups for entrance in lookup[group]]
else:
ret = [entrance for group in groups for entrance in lookup[group]]
self._random.shuffle(ret)
return ret
def __len__(self):
return len(self.dead_ends) + len(self.others)
class ERPlacementState:
"""The state of an ongoing or completed entrance randomization"""
placements: list[Entrance]
"""The list of randomized Entrance objects which have been connected successfully"""
pairings: list[tuple[str, str]]
"""A list of pairings of connected entrance names, of the form (source_exit, target_entrance)"""
world: World
"""The world which is having its entrances randomized"""
collection_state: CollectionState
"""The CollectionState backing the entrance randomization logic"""
coupled: bool
"""Whether entrance randomization is operating in coupled mode"""
def __init__(self, world: World, coupled: bool):
self.placements = []
self.pairings = []
self.world = world
self.coupled = coupled
self.collection_state = world.multiworld.get_all_state(False, True)
@property
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
def find_placeable_exits(self, check_validity: bool) -> list[Entrance]:
if check_validity:
blocked_connections = self.collection_state.blocked_connections[self.world.player]
blocked_connections = sorted(blocked_connections, key=lambda x: x.name)
placeable_randomized_exits = [connection for connection in blocked_connections
if not connection.connected_region
and connection.is_valid_source_transition(self)]
else:
# this is on a beaten minimal attempt, so any exit anywhere is fair game
placeable_randomized_exits = [ex for region in self.world.multiworld.get_regions(self.world.player)
for ex in region.exits if not ex.connected_region]
self.world.random.shuffle(placeable_randomized_exits)
return placeable_randomized_exits
def _connect_one_way(self, source_exit: Entrance, target_entrance: Entrance) -> None:
target_region = target_entrance.connected_region
target_region.entrances.remove(target_entrance)
source_exit.connect(target_region)
self.collection_state.stale[self.world.player] = True
self.placements.append(source_exit)
self.pairings.append((source_exit.name, target_entrance.name))
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance) -> bool:
copied_state = self.collection_state.copy()
# simulated connection. A real connection is unsafe because the region graph is shallow-copied and would
# propagate back to the real multiworld.
copied_state.reachable_regions[self.world.player].add(target_entrance.connected_region)
copied_state.blocked_connections[self.world.player].remove(source_exit)
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
copied_state.update_reachable_regions(self.world.player)
copied_state.sweep_for_advancements()
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
available_randomized_exits = copied_state.blocked_connections[self.world.player]
for _exit in available_randomized_exits:
if _exit.connected_region:
continue
# ignore the source exit, and, if coupled, the reverse exit. They're not actually new
if _exit.name == source_exit.name or (self.coupled and _exit.name == target_entrance.name):
continue
# technically this should be is_valid_source_transition, but that may rely on side effects from
# on_connect, which have not happened here (because we didn't do a real connection, and if we did, we would
# not want them to persist). can_reach is a close enough approximation most of the time.
if _exit.can_reach(copied_state):
return True
return False
def connect(
self,
source_exit: Entrance,
target_entrance: Entrance
) -> tuple[list[Entrance], list[Entrance]]:
"""
Connects a source exit to a target entrance in the graph, accounting for coupling
:returns: The newly placed exits and the dummy entrance(s) which were removed from the graph
"""
source_region = source_exit.parent_region
target_region = target_entrance.connected_region
self._connect_one_way(source_exit, target_entrance)
# if we're doing coupled randomization place the reverse transition as well.
if self.coupled and source_exit.randomization_type == EntranceType.TWO_WAY:
for reverse_entrance in source_region.entrances:
if reverse_entrance.name == source_exit.name:
if reverse_entrance.parent_region:
raise EntranceRandomizationError(
f"Could not perform coupling on {source_exit.name} -> {target_entrance.name} "
f"because the reverse entrance is already parented to "
f"{reverse_entrance.parent_region.name}.")
break
else:
raise EntranceRandomizationError(f"Two way exit {source_exit.name} had no corresponding entrance in "
f"{source_exit.parent_region.name}")
for reverse_exit in target_region.exits:
if reverse_exit.name == target_entrance.name:
if reverse_exit.connected_region:
raise EntranceRandomizationError(
f"Could not perform coupling on {source_exit.name} -> {target_entrance.name} "
f"because the reverse exit is already connected to "
f"{reverse_exit.connected_region.name}.")
break
else:
raise EntranceRandomizationError(f"Two way entrance {target_entrance.name} had no corresponding exit "
f"in {target_region.name}.")
self._connect_one_way(reverse_exit, reverse_entrance)
return [source_exit, reverse_exit], [target_entrance, reverse_entrance]
return [source_exit], [target_entrance]
def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], list[int]]) \
-> dict[int, list[int]]:
"""
Applies a transformation to all known entrance groups on randomizable exists to build a group lookup table.
:param world: Your World instance
:param get_target_groups: Function to call that returns the groups that a specific group type is allowed to
connect to
"""
unique_groups = { entrance.randomization_group for entrance in world.multiworld.get_entrances(world.player)
if entrance.parent_region and not entrance.connected_region }
return { group: get_target_groups(group) for group in unique_groups }
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
"""
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
in randomize_entrances. This should be done after setting the type and group of the entrance.
:param entrance: The entrance which will be disconnected in preparation for randomization.
:param target_group: The group to assign to the created ER target. If not specified, the group from
the original entrance will be copied.
"""
child_region = entrance.connected_region
parent_region = entrance.parent_region
# disconnect the edge
child_region.entrances.remove(entrance)
entrance.connected_region = None
# create the needed ER target
if entrance.randomization_type == EntranceType.TWO_WAY:
# for 2-ways, create a target in the parent region with a matching name to support coupling.
# targets in the child region will be created when the other direction edge is disconnected
target = parent_region.create_er_target(entrance.name)
else:
# for 1-ways, the child region needs a target and coupling/naming is not a concern
target = child_region.create_er_target(child_region.name)
target.randomization_type = entrance.randomization_type
target.randomization_group = target_group or entrance.randomization_group
def randomize_entrances(
world: World,
coupled: bool,
target_group_lookup: dict[int, list[int]],
preserve_group_order: bool = False,
er_targets: list[Entrance] | None = None,
exits: list[Entrance] | None = None,
on_connect: Callable[[ERPlacementState, list[Entrance]], None] | None = None
) -> ERPlacementState:
"""
Randomizes Entrances for a single world in the multiworld.
:param world: Your World instance
:param coupled: Whether connected entrances should be coupled to go in both directions
:param target_group_lookup: Map from each group to a list of the groups that it can be connect to. Every group
used on an exit must be provided and must map to at least one other group. The default
group is 0.
:param preserve_group_order: Whether the order of groupings should be preserved for the returned target_groups
:param er_targets: The list of ER targets (Entrance objects with no parent region) to use for randomization.
Remember to be deterministic! If not provided, automatically discovers all valid targets
in your world.
:param exits: The list of exits (Entrance objects with no target region) to use for randomization.
Remember to be deterministic! If not provided, automatically discovers all valid exits in your world.
:param on_connect: A callback function which allows specifying side effects after a placement is completed
successfully and the underlying collection state has been updated.
"""
if not world.explicit_indirect_conditions:
raise EntranceRandomizationError("Entrance randomization requires explicit indirect conditions in order "
+ "to correctly analyze whether dead end regions can be required in logic.")
start_time = time.perf_counter()
er_state = ERPlacementState(world, coupled)
entrance_lookup = EntranceLookup(world.random, coupled)
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True
def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
placed_exits, removed_entrances = er_state.connect(source_exit, target_entrance)
# remove the placed targets from consideration
for entrance in removed_entrances:
entrance_lookup.remove(entrance)
# propagate new connections
er_state.collection_state.update_reachable_regions(world.player)
er_state.collection_state.sweep_for_advancements()
if on_connect:
on_connect(er_state, placed_exits)
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
nonlocal perform_validity_check
placeable_exits = er_state.find_placeable_exits(perform_validity_check)
for source_exit in placeable_exits:
target_groups = target_group_lookup[source_exit.randomization_group]
for target_entrance in entrance_lookup.get_targets(target_groups, dead_end, preserve_group_order):
# when requiring new exits, ideally we would like to make it so that every placement increases
# (or keeps the same number of) reachable exits. The goal is to continue to expand the search space
# so that we do not crash. In the interest of performance and bias reduction, generally, just checking
# that we are going to a new region is a good approximation. however, we should take extra care on the
# very last exit and check whatever exits we open up are functionally accessible.
# this requirement can be ignored on a beaten minimal, islands are no issue there.
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
or target_entrance.connected_region not in er_state.placed_regions)
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
and len(placeable_exits) == 1)
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
if (needs_speculative_sweep
and not er_state.test_speculative_connection(source_exit, target_entrance)):
continue
do_placement(source_exit, target_entrance)
return True
else:
# no source exits had any valid target so this stage is deadlocked. retries may be implemented if early
# deadlocking is a frequent issue.
lookup = entrance_lookup.dead_ends if dead_end else entrance_lookup.others
# if we're in a stage where we're trying to get to new regions, we could also enter this
# branch in a success state (when all regions of the preferred type have been placed, but there are still
# additional unplaced entrances into those regions)
if require_new_exits:
if all(e.connected_region in er_state.placed_regions for e in lookup):
return False
# if we're on minimal accessibility and can guarantee the game is beatable,
# we can prevent a failure by bypassing future validity checks. this check may be
# expensive; fortunately we only have to do it once
if perform_validity_check and world.options.accessibility == Accessibility.option_minimal \
and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
# ensure that we have enough locations to place our progression
accessible_location_count = 0
prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
# short-circuit location checking in this case
if prog_item_count == 0:
return True
for region in er_state.placed_regions:
for loc in region.locations:
if loc.can_reach(er_state.collection_state):
accessible_location_count += 1
if accessible_location_count >= prog_item_count:
perform_validity_check = False
# pretend that this was successful to retry the current stage
return True
unplaced_entrances = [entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region]
unplaced_exits = [exit_ for region in world.multiworld.get_regions(world.player)
for exit_ in region.exits if not exit_.connected_region]
entrance_kind = "dead ends" if dead_end else "non-dead ends"
region_access_requirement = "requires" if require_new_exits else "does not require"
raise EntranceRandomizationError(
f"None of the available entrances are valid targets for the available exits.\n"
f"Randomization stage is placing {entrance_kind} and {region_access_requirement} "
f"new region/exit access by default\n"
f"Placeable entrances: {lookup}\n"
f"Placeable exits: {placeable_exits}\n"
f"All unplaced entrances: {unplaced_entrances}\n"
f"All unplaced exits: {unplaced_exits}")
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
# stage 1 - try to place all the non-dead-end entrances
while entrance_lookup.others:
if not find_pairing(dead_end=False, require_new_exits=True):
break
# stage 2 - try to place all the dead-end entrances
while entrance_lookup.dead_ends:
if not find_pairing(dead_end=True, require_new_exits=True):
break
# stage 3 - all the regions should be placed at this point. We now need to connect dangling edges
# stage 3a - get the rest of the dead ends (e.g. second entrances into already-visited regions)
# doing this before the non-dead-ends is important to ensure there are enough connections to
# go around
while entrance_lookup.dead_ends:
find_pairing(dead_end=True, require_new_exits=False)
# stage 3b - tie all the other loose ends connecting visited regions to each other
while entrance_lookup.others:
find_pairing(dead_end=False, require_new_exits=False)
running_time = time.perf_counter() - start_time
if running_time > 1.0:
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player},"
f"named {world.multiworld.player_name[world.player]}")
return er_state

65
kvui.py
View File

@@ -40,7 +40,7 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
@@ -64,6 +64,7 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -305,6 +306,50 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class AutocompleteHintInput(TextInput):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
for item_name in item_names:
try:
index = item_name.lower().index(lowered)
except ValueError:
pass # substring not found
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
@@ -570,8 +615,10 @@ class GameManager(App):
# show Archipelago tab if other logging is present
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
hint_panel = self.add_client_tab("Hints", HintLayout())
self.hint_log = HintLog(self.json_to_kivy_parser)
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
@@ -698,7 +745,7 @@ class GameManager(App):
def update_hints(self):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.log_panels["Hints"].refresh_hints(hints)
self.hint_log.refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
@@ -753,6 +800,17 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",
HintStatus.HINT_UNSPECIFIED: "Unspecified",
@@ -769,6 +827,7 @@ status_colors: typing.Dict[HintStatus, str] = {
}
class HintLog(RecycleView):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},

View File

@@ -7,7 +7,7 @@ schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.8.30
certifi>=2024.12.14
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7

View File

@@ -0,0 +1,387 @@
import unittest
from enum import IntEnum
from BaseClasses import Region, EntranceType, MultiWorld, Entrance
from entrance_rando import disconnect_entrance_for_randomization, randomize_entrances, EntranceRandomizationError, \
ERPlacementState, EntranceLookup, bake_target_group_lookup
from Options import Accessibility
from test.general import generate_test_multiworld, generate_locations, generate_items
from worlds.generic.Rules import set_rule
class ERTestGroups(IntEnum):
LEFT = 1
RIGHT = 2
TOP = 3
BOTTOM = 4
directionally_matched_group_lookup = {
ERTestGroups.LEFT: [ERTestGroups.RIGHT],
ERTestGroups.RIGHT: [ERTestGroups.LEFT],
ERTestGroups.TOP: [ERTestGroups.BOTTOM],
ERTestGroups.BOTTOM: [ERTestGroups.TOP]
}
def generate_entrance_pair(region: Region, name_suffix: str, group: int):
lx = region.create_exit(region.name + name_suffix)
lx.randomization_group = group
lx.randomization_type = EntranceType.TWO_WAY
le = region.create_er_target(region.name + name_suffix)
le.randomization_group = group
le.randomization_type = EntranceType.TWO_WAY
def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0,
region_type: type[Region] = Region):
"""
Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each
region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the
bottom right
"""
for row in range(grid_side_length):
for col in range(grid_side_length):
index = row * grid_side_length + col
name = f"region{index}"
region = region_type(name, 1, multiworld)
multiworld.regions.append(region)
generate_locations(region_size, 1, region=region, tag=f"_{name}")
if row == 0 and col == 0:
multiworld.get_region("Menu", 1).connect(region)
if col != 0:
generate_entrance_pair(region, "_left", ERTestGroups.LEFT)
if col != grid_side_length - 1:
generate_entrance_pair(region, "_right", ERTestGroups.RIGHT)
if row != 0:
generate_entrance_pair(region, "_top", ERTestGroups.TOP)
if row != grid_side_length - 1:
generate_entrance_pair(region, "_bottom", ERTestGroups.BOTTOM)
class TestEntranceLookup(unittest.TestCase):
def test_shuffled_targets(self):
"""tests that get_targets shuffles targets between groups when requested"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets:
lookup.add(entrance)
retrieved_targets = lookup.get_targets([ERTestGroups.TOP, ERTestGroups.BOTTOM],
False, False)
prev = None
group_order = [prev := group.randomization_group for group in retrieved_targets
if prev != group.randomization_group]
# technically possible that group order may not be shuffled, by some small chance, on some seeds. but generally
# a shuffled list should alternate more frequently which is the desired behavior here
self.assertGreater(len(group_order), 2)
def test_ordered_targets(self):
"""tests that get_targets does not shuffle targets between groups when requested"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets:
lookup.add(entrance)
retrieved_targets = lookup.get_targets([ERTestGroups.TOP, ERTestGroups.BOTTOM],
False, True)
prev = None
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
class TestBakeTargetGroupLookup(unittest.TestCase):
def test_lookup_generation(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
world = multiworld.worlds[1]
expected = {
ERTestGroups.LEFT: [-ERTestGroups.LEFT],
ERTestGroups.RIGHT: [-ERTestGroups.RIGHT],
ERTestGroups.TOP: [-ERTestGroups.TOP],
ERTestGroups.BOTTOM: [-ERTestGroups.BOTTOM]
}
actual = bake_target_group_lookup(world, lambda g: [-g])
self.assertEqual(expected, actual)
class TestDisconnectForRandomization(unittest.TestCase):
def test_disconnect_default_2way(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.TWO_WAY
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
self.assertIsNone(e.connected_region)
self.assertEqual([], r2.entrances)
self.assertEqual(1, len(r1.exits))
self.assertEqual(e, r1.exits[0])
self.assertEqual(1, len(r1.entrances))
self.assertIsNone(r1.entrances[0].parent_region)
self.assertEqual("e", r1.entrances[0].name)
self.assertEqual(EntranceType.TWO_WAY, r1.entrances[0].randomization_type)
self.assertEqual(1, r1.entrances[0].randomization_group)
def test_disconnect_default_1way(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
self.assertEqual(1, len(r1.exits))
self.assertEqual(e, r1.exits[0])
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(1, r2.entrances[0].randomization_group)
def test_disconnect_uses_alternate_group(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e, 2)
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
self.assertEqual(1, len(r1.exits))
self.assertEqual(e, r1.exits[0])
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(2, r2.entrances[0].randomization_group)
class TestRandomizeEntrances(unittest.TestCase):
def test_determinism(self):
"""tests that the same output is produced for the same input"""
multiworld1 = generate_test_multiworld()
generate_disconnected_region_grid(multiworld1, 5)
multiworld2 = generate_test_multiworld()
generate_disconnected_region_grid(multiworld2, 5)
result1 = randomize_entrances(multiworld1.worlds[1], False, directionally_matched_group_lookup)
result2 = randomize_entrances(multiworld2.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual(result1.pairings, result2.pairings)
for e1, e2 in zip(result1.placements, result2.placements):
self.assertEqual(e1.name, e2.name)
self.assertEqual(e1.parent_region.name, e1.parent_region.name)
self.assertEqual(e1.connected_region.name, e2.connected_region.name)
def test_all_entrances_placed(self):
"""tests that all entrances and exits were placed, all regions are connected, and no dangling edges exist"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
# 5x5 grid + menu
self.assertEqual(26, len(result.placed_regions))
self.assertEqual(80, len(result.pairings))
self.assertEqual(80, len(result.placements))
def test_coupling(self):
"""tests that in coupled mode, all 2 way transitions have an inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
seen_placement_count = 0
def verify_coupled(_: ERPlacementState, placed_entrances: list[Entrance]):
nonlocal seen_placement_count
seen_placement_count += len(placed_entrances)
self.assertEqual(2, len(placed_entrances))
self.assertEqual(placed_entrances[0].parent_region, placed_entrances[1].connected_region)
self.assertEqual(placed_entrances[1].parent_region, placed_entrances[0].connected_region)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup,
on_connect=verify_coupled)
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_uncoupled(self):
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
seen_placement_count = 0
def verify_uncoupled(state: ERPlacementState, placed_entrances: list[Entrance]):
nonlocal seen_placement_count
seen_placement_count += len(placed_entrances)
self.assertEqual(1, len(placed_entrances))
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup,
on_connect=verify_uncoupled)
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_oneway_twoway_pairing(self):
"""tests that 1 ways are only paired to 1 ways and 2 ways are only paired to 2 ways"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
region26 = Region("region26", 1, multiworld)
multiworld.regions.append(region26)
for index, region in enumerate(["region4", "region20", "region24"]):
x = multiworld.get_region(region, 1).create_exit(f"{region}_bottom_1way")
x.randomization_type = EntranceType.ONE_WAY
x.randomization_group = ERTestGroups.BOTTOM
e = region26.create_er_target(f"region26_top_1way{index}")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = ERTestGroups.TOP
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
for exit_name, entrance_name in result.pairings:
# we have labeled our entrances in such a way that all the 1 way entrances have 1way in the name,
# so test for that since the ER target will have been discarded
if "1way" in exit_name:
self.assertIn("1way", entrance_name)
def test_group_constraints_satisfied(self):
"""tests that all grouping constraints are satisfied"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
for exit_name, entrance_name in result.pairings:
# we have labeled our entrances in such a way that all the entrances contain their group in the name
# so test for that since the ER target will have been discarded
if "top" in exit_name:
self.assertIn("bottom", entrance_name)
if "bottom" in exit_name:
self.assertIn("top", entrance_name)
if "left" in exit_name:
self.assertIn("right", entrance_name)
if "right" in exit_name:
self.assertIn("left", entrance_name)
def test_minimal_entrance_rando(self):
"""tests that entrance randomization can complete with minimal accessibility and unreachable exits"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(10, 1, True)
multiworld.itempool += prog_items
filler_items = generate_items(15, 1, False)
multiworld.itempool += filler_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_restrictive_region_requirement_does_not_fail(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 2, 1)
region = Region("region4", 1, multiworld)
multiworld.regions.append(region)
generate_entrance_pair(multiworld.get_region("region0", 1), "_right2", ERTestGroups.RIGHT)
generate_entrance_pair(region, "_left", ERTestGroups.LEFT)
blocked_exits = ["region1_left", "region1_bottom",
"region2_top", "region2_right",
"region3_left", "region3_top"]
for exit_name in blocked_exits:
blocked_exit = multiworld.get_entrance(exit_name, 1)
blocked_exit.access_rule = lambda state: state.can_reach_region("region4", 1)
multiworld.register_indirect_condition(region, blocked_exit)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
# verifying that we did in fact place region3 adjacent to region0 to unblock all the other connections
# (and implicitly, that ER didn't fail)
self.assertTrue(("region0_right", "region4_left") in result.pairings
or ("region0_right2", "region4_left") in result.pairings)
def test_fails_when_mismatched_entrance_and_exit_count(self):
"""tests that entrance randomization fast-fails if the input exit and entrance count do not match"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
multiworld.get_region("region1", 1).create_exit("extra")
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)
def test_fails_when_some_unreachable_exit(self):
"""tests that entrance randomization fails if an exit is never reachable (non-minimal accessibility)"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)
def test_fails_when_some_unconnectable_exit(self):
"""tests that entrance randomization fails if an exit can't be made into a valid placement (non-minimal)"""
class CustomEntrance(Entrance):
def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool:
if other.name == "region1_right":
return False
class CustomRegion(Region):
entrance_type = CustomEntrance
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5, region_type=CustomRegion)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)
def test_minimal_er_fails_when_not_enough_locations_to_fit_progression(self):
"""
tests that entrance randomization fails in minimal accessibility if there are not enough locations
available to place all progression items locally
"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(30, 1, True)
multiworld.itempool += prog_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)

View File

@@ -52,3 +52,68 @@ class TestImplemented(unittest.TestCase):
def test_no_failed_world_loads(self):
if failed_world_loads:
self.fail(f"The following worlds failed to load: {failed_world_loads}")
def test_explicit_indirect_conditions_spheres(self):
"""Tests that worlds using explicit indirect conditions produce identical spheres as when using implicit
indirect conditions"""
# Because the iteration order of blocked_connections in CollectionState.update_reachable_regions() is
# nondeterministic, this test may sometimes pass with the same seed even when there are missing indirect
# conditions.
for game_name, world_type in AutoWorldRegister.world_types.items():
multiworld = setup_solo_multiworld(world_type)
world = multiworld.get_game_worlds(game_name)[0]
if not world.explicit_indirect_conditions:
# The world does not use explicit indirect conditions, so it can be skipped.
continue
# The world may override explicit_indirect_conditions as a property that cannot be set, so try modifying it.
try:
world.explicit_indirect_conditions = False
world.explicit_indirect_conditions = True
except Exception:
# Could not modify the attribute, so skip this world.
with self.subTest(game=game_name, skipped="world.explicit_indirect_conditions could not be set"):
continue
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
# is nondeterministic and may vary between runs with the same seed.
explicit_spheres = list(multiworld.get_spheres())
# Disable explicit indirect conditions and produce a second list of spheres.
world.explicit_indirect_conditions = False
implicit_spheres = list(multiworld.get_spheres())
# Both lists should be identical.
if explicit_spheres == implicit_spheres:
# Test passed.
continue
# Find the first sphere that was different and provide a useful failure message.
zipped = zip(explicit_spheres, implicit_spheres)
for sphere_num, (sphere_explicit, sphere_implicit) in enumerate(zipped, start=1):
# Each sphere created with explicit indirect conditions should be identical to the sphere created
# with implicit indirect conditions.
if sphere_explicit != sphere_implicit:
reachable_only_with_implicit = sorted(sphere_implicit - sphere_explicit)
if reachable_only_with_implicit:
locations_and_parents = [(loc, loc.parent_region) for loc in reachable_only_with_implicit]
self.fail(f"Sphere {sphere_num} created with explicit indirect conditions did not contain"
f" the same locations as sphere {sphere_num} created with implicit indirect"
f" conditions. There may be missing indirect conditions for connections to the"
f" locations' parent regions or connections from other regions which connect to"
f" these regions."
f"\nLocations that should have been reachable in sphere {sphere_num} and their"
f" parent regions:"
f"\n{locations_and_parents}")
else:
# Some locations were only present in the sphere created with explicit indirect conditions.
# This should not happen because missing indirect conditions should only reduce
# accessibility, not increase accessibility.
reachable_only_with_explicit = sorted(sphere_explicit - sphere_implicit)
self.fail(f"Sphere {sphere_num} created with explicit indirect conditions contained more"
f" locations than sphere {sphere_num} created with implicit indirect conditions."
f" This should not happen."
f"\nUnexpectedly reachable locations in sphere {sphere_num}:"
f"\n{reachable_only_with_explicit}")
self.fail("Unreachable")

View File

@@ -86,3 +86,7 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
""" override this with implementation to kill player """
pass
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
""" override this with code to handle packages from the server """
pass

View File

@@ -7,7 +7,7 @@ import sys
import time
from random import Random
from dataclasses import make_dataclass
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple,
TYPE_CHECKING, Type, Union)
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
@@ -534,12 +534,24 @@ class World(metaclass=AutoWorldRegister):
def get_location(self, location_name: str) -> "Location":
return self.multiworld.get_location(location_name, self.player)
def get_locations(self) -> "Iterable[Location]":
return self.multiworld.get_locations(self.player)
def get_entrance(self, entrance_name: str) -> "Entrance":
return self.multiworld.get_entrance(entrance_name, self.player)
def get_entrances(self) -> "Iterable[Entrance]":
return self.multiworld.get_entrances(self.player)
def get_region(self, region_name: str) -> "Region":
return self.multiworld.get_region(region_name, self.player)
def get_regions(self) -> "Iterable[Region]":
return self.multiworld.get_regions(self.player)
def push_precollected(self, item: Item) -> None:
self.multiworld.push_precollected(item)
@property
def player_name(self) -> str:
return self.multiworld.get_player_name(self.player)

View File

@@ -18,16 +18,42 @@ class Type(Enum):
class Component:
"""
A Component represents a process launchable by Archipelago Launcher, either by a User action in the GUI,
by resolving an archipelago://user:pass@host:port link from the WebHost, by resolving a patch file's metadata,
or by using a component name arg while running the Launcher in CLI i.e. `ArchipelagoLauncher.exe "Text Client"`
Expected to be appended to LauncherComponents.component list to be used.
"""
display_name: str
"""Used as the GUI button label and the component name in the CLI args"""
type: Type
"""
Enum "Type" classification of component intent, for filtering in the Launcher GUI
If not set in the constructor, it will be inferred by display_name
"""
script_name: Optional[str]
"""Recommended to use func instead; Name of file to run when the component is called"""
frozen_name: Optional[str]
"""Recommended to use func instead; Name of the frozen executable file for this component"""
icon: str # just the name, no suffix
"""Lookup ID for the icon path in LauncherComponents.icon_paths"""
cli: bool
"""Bool to control if the component gets launched in an appropriate Terminal for the OS"""
func: Optional[Callable]
"""
Function that gets called when the component gets launched
Any arg besides the component name arg is passed into the func as well, so handling *args is suggested
"""
file_identifier: Optional[Callable[[str], bool]]
"""
Function that is run against patch file arg to identify which component is appropriate to launch
If the function is an Instance of SuffixIdentifier the suffixes will also be valid for the Open Patch component
"""
game_name: Optional[str]
"""Game name to identify component when handling launch links from WebHost"""
supports_uri: Optional[bool]
"""Bool to identify if a component supports being launched by launch links from WebHost"""
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,

View File

@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))

View File

@@ -130,19 +130,21 @@ class TestGanonsTower(TestDungeon):
["Ganons Tower - Pre-Moldorm Chest", False, []],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])

View File

@@ -3,11 +3,11 @@
## Required Software
- The original Aquaria Game (purchasable from most online game stores)
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
## Optional Software
- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)

View File

@@ -3,12 +3,11 @@
## Logiciels nécessaires
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Le client du Randomizer d'Aquaria [Aquaria randomizer]
(https://github.com/tioui/Aquaria_Randomizer/releases)
- Le client du Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
## Logiciels optionnels
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Procédures d'installation et d'exécution

View File

@@ -4,14 +4,17 @@ import random
class ChoiceIsRandom(Choice):
randomized: bool = False
randomized: bool
def __init__(self, value: int, randomized: bool = False):
super().__init__(value)
self.randomized = randomized
@classmethod
def from_text(cls, text: str) -> Choice:
text = text.lower()
if text == "random":
cls.randomized = True
return cls(random.choice(list(cls.name_lookup)))
return cls(random.choice(list(cls.name_lookup)), True)
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)

View File

@@ -1366,7 +1366,8 @@ class DarkSouls3World(World):
text = "\n" + text + "\n"
spoiler_handle.write(text)
def post_fill(self):
@classmethod
def stage_post_fill(cls, multiworld: MultiWorld):
"""If item smoothing is enabled, rearrange items so they scale up smoothly through the run.
This determines the approximate order a given silo of items (say, soul items) show up in the
@@ -1375,106 +1376,125 @@ class DarkSouls3World(World):
items, later spheres get higher-level ones. Within a sphere, items in DS3 are distributed in
region order, and then the best items in a sphere go into the multiworld.
"""
ds3_worlds = [world for world in cast(List[DarkSouls3World], multiworld.get_game_worlds(cls.game)) if
world.options.smooth_upgrade_items
or world.options.smooth_soul_items
or world.options.smooth_upgraded_weapons]
if not ds3_worlds:
# No worlds need item smoothing.
return
locations_by_sphere = [
sorted(loc for loc in sphere if loc.item.player == self.player and not loc.locked)
for sphere in self.multiworld.get_spheres()
]
spheres_per_player: Dict[int, List[List[Location]]] = {world.player: [] for world in ds3_worlds}
for sphere in multiworld.get_spheres():
locations_per_item_player: Dict[int, List[Location]] = {player: [] for player in spheres_per_player.keys()}
for location in sphere:
if location.locked:
continue
item_player = location.item.player
if item_player in locations_per_item_player:
locations_per_item_player[item_player].append(location)
for player, locations in locations_per_item_player.items():
# Sort for deterministic results.
locations.sort()
spheres_per_player[player].append(locations)
# All items in the base game in approximately the order they appear
all_item_order: List[DS3ItemData] = [
item_dictionary[location.default_item_name]
for region in region_order
# Shuffle locations within each region.
for location in self._shuffle(location_tables[region])
if self._is_location_available(location)
]
for ds3_world in ds3_worlds:
locations_by_sphere = spheres_per_player[ds3_world.player]
# All DarkSouls3Items for this world that have been assigned anywhere, grouped by name
full_items_by_name: Dict[str, List[DarkSouls3Item]] = defaultdict(list)
for location in self.multiworld.get_filled_locations():
if location.item.player == self.player and (
location.player != self.player or self._is_location_available(location)
):
full_items_by_name[location.item.name].append(location.item)
# All items in the base game in approximately the order they appear
all_item_order: List[DS3ItemData] = [
item_dictionary[location.default_item_name]
for region in region_order
# Shuffle locations within each region.
for location in ds3_world._shuffle(location_tables[region])
if ds3_world._is_location_available(location)
]
def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None:
"""Rearrange all items in item_order to match that order.
# All DarkSouls3Items for this world that have been assigned anywhere, grouped by name
full_items_by_name: Dict[str, List[DarkSouls3Item]] = defaultdict(list)
for location in multiworld.get_filled_locations():
if location.item.player == ds3_world.player and (
location.player != ds3_world.player or ds3_world._is_location_available(location)
):
full_items_by_name[location.item.name].append(location.item)
Note: this requires that item_order exactly matches the number of placed items from this
world matching the given names.
"""
def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None:
"""Rearrange all items in item_order to match that order.
# Convert items to full DarkSouls3Items.
converted_item_order: List[DarkSouls3Item] = [
item for item in (
(
# full_items_by_name won't contain DLC items if the DLC is disabled.
(full_items_by_name[item.name] or [None]).pop(0)
if isinstance(item, DS3ItemData) else item
Note: this requires that item_order exactly matches the number of placed items from this
world matching the given names.
"""
# Convert items to full DarkSouls3Items.
converted_item_order: List[DarkSouls3Item] = [
item for item in (
(
# full_items_by_name won't contain DLC items if the DLC is disabled.
(full_items_by_name[item.name] or [None]).pop(0)
if isinstance(item, DS3ItemData) else item
)
for item in item_order
)
for item in item_order
)
# Never re-order event items, because they weren't randomized in the first place.
if item and item.code is not None
]
# Never re-order event items, because they weren't randomized in the first place.
if item and item.code is not None
]
names = {item.name for item in converted_item_order}
names = {item.name for item in converted_item_order}
all_matching_locations = [
loc
for sphere in locations_by_sphere
for loc in sphere
if loc.item.name in names
]
all_matching_locations = [
loc
for sphere in locations_by_sphere
for loc in sphere
if loc.item.name in names
]
# It's expected that there may be more total items than there are matching locations if
# the player has chosen a more limited accessibility option, since the matching
# locations *only* include items in the spheres of accessibility.
if len(converted_item_order) < len(all_matching_locations):
raise Exception(
f"DS3 bug: there are {len(all_matching_locations)} locations that can " +
f"contain smoothed items, but only {len(converted_item_order)} items to smooth."
)
# It's expected that there may be more total items than there are matching locations if
# the player has chosen a more limited accessibility option, since the matching
# locations *only* include items in the spheres of accessibility.
if len(converted_item_order) < len(all_matching_locations):
raise Exception(
f"DS3 bug: there are {len(all_matching_locations)} locations that can " +
f"contain smoothed items, but only {len(converted_item_order)} items to smooth."
)
for sphere in locations_by_sphere:
locations = [loc for loc in sphere if loc.item.name in names]
for sphere in locations_by_sphere:
locations = [loc for loc in sphere if loc.item.name in names]
# Check the game, not the player, because we know how to sort within regions for DS3
offworld = self._shuffle([loc for loc in locations if loc.game != "Dark Souls III"])
onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"),
key=lambda loc: loc.data.region_value)
# Check the game, not the player, because we know how to sort within regions for DS3
offworld = ds3_world._shuffle([loc for loc in locations if loc.game != "Dark Souls III"])
onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"),
key=lambda loc: loc.data.region_value)
# Give offworld regions the last (best) items within a given sphere
for location in onworld + offworld:
new_item = self._pop_item(location, converted_item_order)
location.item = new_item
new_item.location = location
# Give offworld regions the last (best) items within a given sphere
for location in onworld + offworld:
new_item = ds3_world._pop_item(location, converted_item_order)
location.item = new_item
new_item.location = location
if self.options.smooth_upgrade_items:
base_names = {
"Titanite Shard", "Large Titanite Shard", "Titanite Chunk", "Titanite Slab",
"Titanite Scale", "Twinkling Titanite", "Farron Coal", "Sage's Coal", "Giant's Coal",
"Profaned Coal"
}
smooth_items([item for item in all_item_order if item.base_name in base_names])
if ds3_world.options.smooth_upgrade_items:
base_names = {
"Titanite Shard", "Large Titanite Shard", "Titanite Chunk", "Titanite Slab",
"Titanite Scale", "Twinkling Titanite", "Farron Coal", "Sage's Coal", "Giant's Coal",
"Profaned Coal"
}
smooth_items([item for item in all_item_order if item.base_name in base_names])
if self.options.smooth_soul_items:
smooth_items([
item for item in all_item_order
if item.souls and item.classification != ItemClassification.progression
])
if ds3_world.options.smooth_soul_items:
smooth_items([
item for item in all_item_order
if item.souls and item.classification != ItemClassification.progression
])
if self.options.smooth_upgraded_weapons:
upgraded_weapons = [
location.item
for location in self.multiworld.get_filled_locations()
if location.item.player == self.player
and location.item.level and location.item.level > 0
and location.item.classification != ItemClassification.progression
]
upgraded_weapons.sort(key=lambda item: item.level)
smooth_items(upgraded_weapons)
if ds3_world.options.smooth_upgraded_weapons:
upgraded_weapons = [
location.item
for location in multiworld.get_filled_locations()
if location.item.player == ds3_world.player
and location.item.level and location.item.level > 0
and location.item.classification != ItemClassification.progression
]
upgraded_weapons.sort(key=lambda item: item.level)
smooth_items(upgraded_weapons)
def _shuffle(self, seq: Sequence) -> List:
"""Returns a shuffled copy of a sequence."""

View File

@@ -37,8 +37,8 @@ base_info = {
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "2.0",
"dependencies": [
"base >= 2.0.15",
"? quality >= 2.0.15",
"base >= 2.0.28",
"? quality >= 2.0.28",
"! space-age",
"? science-not-invited",
"? factory-levels"

View File

@@ -63,17 +63,19 @@ class FactorioElement:
class Technology(FactorioElement): # maybe make subclass of Location?
has_modifier: bool
factorio_id: int
progressive: Tuple[str]
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
modifiers: list[str]
def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (),
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
modifiers: list[str] = None, unlocks: Union[Set[str], bool] = None):
self.name = technology_name
self.factorio_id = factorio_id
self.progressive = progressive
self.has_modifier = has_modifier
if modifiers is None:
modifiers = []
self.modifiers = modifiers
if unlocks:
self.unlocks = unlocks
else:
@@ -82,6 +84,10 @@ class Technology(FactorioElement): # maybe make subclass of Location?
def __hash__(self):
return self.factorio_id
@property
def has_modifier(self) -> bool:
return bool(self.modifiers)
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
return CustomTechnology(self, world, allowed_packs, player)
@@ -191,13 +197,14 @@ class Machine(FactorioElement):
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
mining_with_fluid_sources: set[str] = set()
# recipes and technologies can share names in Factorio
for technology_name, data in sorted(techs_future.result().items()):
technology = Technology(
technology_name,
factorio_tech_id,
has_modifier=data["has_modifier"],
modifiers=data.get("modifiers", []),
unlocks=set(data["unlocks"]) - start_unlocked_recipes,
)
factorio_tech_id += 1
@@ -205,7 +212,8 @@ for technology_name, data in sorted(techs_future.result().items()):
technology_table[technology_name] = technology
for recipe_name in technology.unlocks:
recipe_sources.setdefault(recipe_name, set()).add(technology_name)
if "mining-with-fluid" in technology.modifiers:
mining_with_fluid_sources.add(technology_name)
del techs_future
recipes = {}
@@ -221,6 +229,8 @@ for resource_name, resource_data in resources_future.result().items():
"energy": resource_data["mining_time"],
"category": resource_data["category"]
}
if "required_fluid" in resource_data:
recipe_sources.setdefault(f"mining-{resource_name}", set()).update(mining_with_fluid_sources)
del resources_future
for recipe_name, recipe_data in raw_recipes.items():
@@ -431,7 +441,9 @@ for root in sorted_rows:
factorio_tech_id += 1
progressive_technology = Technology(root, factorio_tech_id,
tuple(progressive),
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
modifiers=sorted(set.union(
*(set(technology_table[tech].modifiers) for tech in progressive)
)),
unlocks=any(technology_table[tech].unlocks for tech in progressive),)
progressive_tech_table[root] = progressive_technology.factorio_id
progressive_technology_table[root] = progressive_technology

View File

@@ -445,6 +445,10 @@ end
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
-- Update players when the cutscene is cancelled or finished. (needed for skins_factored)
script.on_event(defines.events.on_cutscene_cancelled, update_player_event)
script.on_event(defines.events.on_cutscene_finished, update_player_event)
function add_samples(force, name, count)
local function add_to_table(t)
if count <= 0 then

View File

@@ -1,6 +1,7 @@
{% from "macros.lua" import dict_to_recipe, variable_to_lua %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib')
data.raw["item"]["rocket-part"].hidden = false
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
{
production_type = "input",
@@ -162,6 +163,7 @@ data.raw["ammo"]["artillery-shell"].stack_size = 10
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name in base_tech_table -%}
technologies["{{ original_tech_name }}"].hidden = true
technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{% endfor %}
{%- for location, item in locations %}
{#- the tech researched by the local player #}

File diff suppressed because one or more lines are too long

View File

@@ -260,7 +260,8 @@ def create_items(self) -> None:
items.append(i)
for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"):
for item in self.item_name_groups[item_group]:
# Sort for deterministic order
for item in sorted(self.item_name_groups[item_group]):
add_item(item)
if self.options.brown_boxes == "include":

View File

@@ -1,7 +1,7 @@
# Final Fantasy Mystic Quest
## Game page in other languages:
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
## Where is the options page?

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 forces a filler or trap item which
isn't necessary for progression into these locations.
* `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.
* `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

View File

@@ -27,6 +27,7 @@ including the exclamation point.
- `!countdown <number of seconds>` Starts a countdown using the given seconds value. Useful for synchronizing starts.
Defaults to 10 seconds if no argument is provided.
- `!alias <alias>` Sets your alias, which allows you to use commands with the alias rather than your provided name.
`!alias` on its own will reset the alias to the player's original name.
- `!admin <command>` Executes a command as if you typed it into the server console. Remote administration must be
enabled.
@@ -65,6 +66,7 @@ including the exclamation point.
argument is provided.
- `/option <option name> <option value>` Set a server option. For a list of options, use the `/options` command.
- `/alias <player name> <alias name>` Assign a player an alias, allowing you to reference the player by the alias in commands.
`!alias <player name>` on its own will reset the alias to the player's original name.
### Collect/Release

View File

@@ -132,7 +132,13 @@ splitter_pattern = re.compile(r'(?<!^)(?=[A-Z])')
for option_name, option_data in pool_options.items():
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
if option_name == "RandomizeFocus":
# pool options for focus are just lying
count = 1
else:
count = len([loc for loc in option_data[1] if loc != "Start"])
extra_data["__doc__"] = option_docstrings[option_name] + \
f"\n This option adds approximately {count} location{'s' if count != 1 else ''}."
if option_name in default_on:
option = type(option_name, (DefaultOnToggle,), extra_data)
else:
@@ -213,6 +219,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
class MinimumEggPrice(Range):
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
Only takes effect if the EggSlotShops option is greater than 0."""
rich_text_doc = False
display_name = "Minimum Egg Price"
range_start = 1
range_end = 20
@@ -222,6 +229,7 @@ class MinimumEggPrice(Range):
class MaximumEggPrice(MinimumEggPrice):
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
Only takes effect if the EggSlotShops option is greater than 0."""
rich_text_doc = False
display_name = "Maximum Egg Price"
default = 10
@@ -265,6 +273,7 @@ class RandomCharmCosts(NamedRange):
Set to -1 or vanilla for vanilla costs.
Set to -2 or shuffle to shuffle around the vanilla costs to different charms."""
rich_text_doc = False
display_name = "Randomize Charm Notch Costs"
range_start = 0
range_end = 240
@@ -294,6 +303,10 @@ class RandomCharmCosts(NamedRange):
return charms
class CharmCost(Range):
range_end = 6
class PlandoCharmCosts(OptionDict):
"""Allows setting a Charm's Notch costs directly, mapping {name: cost}.
This is set after any random Charm Notch costs, if applicable."""
@@ -303,6 +316,27 @@ class PlandoCharmCosts(OptionDict):
Optional(name): And(int, lambda n: 6 >= n >= 0, error="Charm costs must be integers in the range 0-6.") for name in charm_names
})
def __init__(self, value):
# To handle keys of random like other options, create an option instance from their values
# Additionally a vanilla keyword is added to plando individual charms to vanilla costs
# and default is disabled so as to not cause confusion
self.value = {}
for key, data in value.items():
if isinstance(data, str):
if data.lower() == "vanilla" and key in self.valid_keys:
self.value[key] = vanilla_costs[charm_names.index(key)]
continue
elif data.lower() == "default":
# default is too easily confused with vanilla but actually 0
# skip CharmCost resolution to fail schema afterwords
self.value[key] = data
continue
try:
self.value[key] = CharmCost.from_any(data).value
except ValueError as ex:
# will fail schema afterwords
self.value[key] = data
def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]:
for name, cost in self.value.items():
charm_costs[charm_names.index(name)] = cost
@@ -412,6 +446,7 @@ class Goal(Choice):
class GrubHuntGoal(NamedRange):
"""The amount of grubs required to finish Grub Hunt.
On 'All' any grubs from item links replacements etc. will be counted"""
rich_text_doc = False
display_name = "Grub Hunt Goal"
range_start = 1
range_end = 46
@@ -421,7 +456,7 @@ class GrubHuntGoal(NamedRange):
class WhitePalace(Choice):
"""
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
required if charms are vanilla.
"""
display_name = "White Palace"
@@ -458,6 +493,7 @@ class DeathLinkShade(Choice):
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
your existing shade, if any.
"""
rich_text_doc = False
option_vanilla = 0
option_shadeless = 1
option_shade = 2
@@ -472,6 +508,7 @@ class DeathLinkBreaksFragileCharms(Toggle):
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
will continue to do so.
"""
rich_text_doc = False
display_name = "Deathlink Breaks Fragile Charms"
@@ -490,6 +527,7 @@ class CostSanity(Choice):
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
"""
rich_text_doc = False
option_off = 0
alias_no = 0
option_on = 1

View File

@@ -134,7 +134,9 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
class HKWeb(WebWorld):
setup_en = Tutorial(
rich_text_options_doc = True
setup_en = Tutorial(
"Mod Setup and Use Guide",
"A guide to playing Hollow Knight with Archipelago.",
"English",
@@ -143,7 +145,7 @@ class HKWeb(WebWorld):
["Ijwu"]
)
setup_pt_br = Tutorial(
setup_pt_br = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Português Brasileiro",

158
worlds/inscryption/Items.py Normal file
View File

@@ -0,0 +1,158 @@
from BaseClasses import ItemClassification
from typing import TypedDict, List
from BaseClasses import Item
base_id = 147000
class InscryptionItem(Item):
name: str = "Inscryption"
class ItemDict(TypedDict):
name: str
count: int
classification: ItemClassification
act1_items: List[ItemDict] = [
{'name': "Stinkbug Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Stunted Wolf Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Wardrobe Key",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Skink Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Ant Cards",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Caged Wolf Card",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Squirrel Totem Head",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Dagger",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Film Roll",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Ring",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Magnificus Eye",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Oil Painting's Clover Plant",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Extra Candle",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Bee Figurine",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Greater Smoke",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Angler Hook",
'count': 1,
'classification': ItemClassification.useful}
]
act2_items: List[ItemDict] = [
{'name': "Camera Replica",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Pile Of Meat",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Epitaph Piece",
'count': 9,
'classification': ItemClassification.progression},
{'name': "Epitaph Pieces",
'count': 3,
'classification': ItemClassification.progression},
{'name': "Monocle",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Bone Lord Femur",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Bone Lord Horn",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Bone Lord Holo Key",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Mycologists Holo Key",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Ancient Obol",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Great Kraken Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Drowned Soul Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Salmon Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Dock's Clover Plant",
'count': 1,
'classification': ItemClassification.useful}
]
act3_items: List[ItemDict] = [
{'name': "Extra Battery",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Nano Armor Generator",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Mrs. Bomb's Remote",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Inspectometer Battery",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Gems Module",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Lonely Wizbot Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Fishbot Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Ourobot Card",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Holo Pelt",
'count': 5,
'classification': ItemClassification.progression},
{'name': "Quill",
'count': 1,
'classification': ItemClassification.progression},
]
filler_items: List[ItemDict] = [
{'name': "Currency",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Card Pack",
'count': 1,
'classification': ItemClassification.filler}
]

View File

@@ -0,0 +1,127 @@
from typing import Dict, List
from BaseClasses import Location
base_id = 147000
class InscryptionLocation(Location):
game: str = "Inscryption"
act1_locations = [
"Act 1 - Boss Prospector",
"Act 1 - Boss Angler",
"Act 1 - Boss Trapper",
"Act 1 - Boss Leshy",
"Act 1 - Safe",
"Act 1 - Clock Main Compartment",
"Act 1 - Clock Upper Compartment",
"Act 1 - Dagger",
"Act 1 - Wardrobe Drawer 1",
"Act 1 - Wardrobe Drawer 2",
"Act 1 - Wardrobe Drawer 3",
"Act 1 - Wardrobe Drawer 4",
"Act 1 - Magnificus Eye",
"Act 1 - Painting 1",
"Act 1 - Painting 2",
"Act 1 - Painting 3",
"Act 1 - Greater Smoke"
]
act2_locations = [
"Act 2 - Boss Leshy",
"Act 2 - Boss Magnificus",
"Act 2 - Boss Grimora",
"Act 2 - Boss P03",
"Act 2 - Battle Prospector",
"Act 2 - Battle Angler",
"Act 2 - Battle Trapper",
"Act 2 - Battle Sawyer",
"Act 2 - Battle Royal",
"Act 2 - Battle Kaycee",
"Act 2 - Battle Goobert",
"Act 2 - Battle Pike Mage",
"Act 2 - Battle Lonely Wizard",
"Act 2 - Battle Inspector",
"Act 2 - Battle Melter",
"Act 2 - Battle Dredger",
"Act 2 - Dock Chest",
"Act 2 - Forest Cabin Chest",
"Act 2 - Forest Meadow Chest",
"Act 2 - Cabin Wardrobe Drawer",
"Act 2 - Cabin Safe",
"Act 2 - Crypt Casket 1",
"Act 2 - Crypt Casket 2",
"Act 2 - Crypt Well",
"Act 2 - Tower Chest 1",
"Act 2 - Tower Chest 2",
"Act 2 - Tower Chest 3",
"Act 2 - Tentacle",
"Act 2 - Factory Trash Can",
"Act 2 - Factory Drawer 1",
"Act 2 - Factory Drawer 2",
"Act 2 - Factory Chest 1",
"Act 2 - Factory Chest 2",
"Act 2 - Factory Chest 3",
"Act 2 - Factory Chest 4",
"Act 2 - Ancient Obol",
"Act 2 - Bone Lord Femur",
"Act 2 - Bone Lord Horn",
"Act 2 - Bone Lord Holo Key",
"Act 2 - Mycologists Holo Key",
"Act 2 - Camera Replica",
"Act 2 - Clover",
"Act 2 - Monocle",
"Act 2 - Epitaph Piece 1",
"Act 2 - Epitaph Piece 2",
"Act 2 - Epitaph Piece 3",
"Act 2 - Epitaph Piece 4",
"Act 2 - Epitaph Piece 5",
"Act 2 - Epitaph Piece 6",
"Act 2 - Epitaph Piece 7",
"Act 2 - Epitaph Piece 8",
"Act 2 - Epitaph Piece 9"
]
act3_locations = [
"Act 3 - Boss Photographer",
"Act 3 - Boss Archivist",
"Act 3 - Boss Unfinished",
"Act 3 - Boss G0lly",
"Act 3 - Boss Mycologists",
"Act 3 - Bone Lord Room",
"Act 3 - Shop Holo Pelt",
"Act 3 - Middle Holo Pelt",
"Act 3 - Forest Holo Pelt",
"Act 3 - Crypt Holo Pelt",
"Act 3 - Tower Holo Pelt",
"Act 3 - Trader 1",
"Act 3 - Trader 2",
"Act 3 - Trader 3",
"Act 3 - Trader 4",
"Act 3 - Trader 5",
"Act 3 - Drawer 1",
"Act 3 - Drawer 2",
"Act 3 - Clock",
"Act 3 - Extra Battery",
"Act 3 - Nano Armor Generator",
"Act 3 - Chest",
"Act 3 - Goobert's Painting",
"Act 3 - Luke's File Entry 1",
"Act 3 - Luke's File Entry 2",
"Act 3 - Luke's File Entry 3",
"Act 3 - Luke's File Entry 4",
"Act 3 - Inspectometer Battery",
"Act 3 - Gems Drone",
"Act 3 - The Great Transcendence",
"Act 3 - Well"
]
regions_to_locations: Dict[str, List[str]] = {
"Menu": [],
"Act 1": act1_locations,
"Act 2": act2_locations,
"Act 3": act3_locations,
"Epilogue": []
}

View File

@@ -0,0 +1,137 @@
from dataclasses import dataclass
from Options import Toggle, Choice, DeathLinkMixin, StartInventoryPool, PerGameCommonOptions, DefaultOnToggle
class Act1DeathLinkBehaviour(Choice):
"""If DeathLink is enabled, determines what counts as a death in act 1. This affects deaths sent and received.
- Sacrificed: Send a death when sacrificed by Leshy. Receiving a death will extinguish all candles.
- Candle Extinguished: Send a death when a candle is extinguished. Receiving a death will extinguish a candle."""
display_name = "Act 1 Death Link Behaviour"
option_sacrificed = 0
option_candle_extinguished = 1
default = 0
class Goal(Choice):
"""Defines the goal to accomplish in order to complete the randomizer.
- Full Story In Order: Complete each act in order. You can return to previously completed acts.
- Full Story Any Order: Complete each act in any order. All acts are available from the start.
- First Act: Complete Act 1 by finding the New Game button. Great for a smaller scale randomizer."""
display_name = "Goal"
option_full_story_in_order = 0
option_full_story_any_order = 1
option_first_act = 2
default = 0
class RandomizeCodes(Toggle):
"""Randomize codes and passwords in the game (clocks, safes, etc.)"""
display_name = "Randomize Codes"
class RandomizeDeck(Choice):
"""Randomize cards in your deck into new cards.
Disable: Disable the feature.
- Every Encounter Within Same Type: Randomize cards within the same type every encounter (keep rarity/scrybe type).
- Every Encounter Any Type: Randomize cards into any possible card every encounter.
- Starting Only: Only randomize cards given at the beginning of runs and acts."""
display_name = "Randomize Deck"
option_disable = 0
option_every_encounter_within_same_type = 1
option_every_encounter_any_type = 2
option_starting_only = 3
default = 0
class RandomizeSigils(Choice):
"""Randomize sigils printed on the cards into new sigils every encounter.
- Disable: Disable the feature.
- Randomize Addons: Only randomize sigils added from sacrifices or other means.
- Randomize All: Randomize all sigils."""
display_name = "Randomize Abilities"
option_disable = 0
option_randomize_addons = 1
option_randomize_all = 2
default = 0
class OptionalDeathCard(Choice):
"""Add a moment after death in act 1 where you can decide to create a death card or not.
- Disable: Disable the feature.
- Always On: The choice is always offered after losing all candles.
- DeathLink Only: The choice is only offered after receiving a DeathLink event."""
display_name = "Optional Death Card"
option_disable = 0
option_always_on = 1
option_deathlink_only = 2
default = 2
class SkipTutorial(DefaultOnToggle):
"""Skips the first few tutorial runs of act 1. Bones are available from the start."""
display_name = "Skip Tutorial"
class SkipEpilogue(Toggle):
"""Completes the goal as soon as the required acts are completed without the need of completing the epilogue."""
display_name = "Skip Epilogue"
class EpitaphPiecesRandomization(Choice):
"""Determines how epitaph pieces in act 2 are randomized. This can affect your chances of getting stuck.
- All Pieces: Randomizes all nine pieces as their own item.
- In Groups: Randomizes pieces in groups of three.
- As One Item: Group all nine pieces as a single item."""
display_name = "Epitaph Pieces Randomization"
option_all_pieces = 0
option_in_groups = 1
option_as_one_item = 2
default = 0
class PaintingChecksBalancing(Choice):
"""Generation options for the second and third painting checks in act 1.
- None: Adds no progression logic to these painting checks. They will all count as sphere 1 (early game checks).
- Balanced: Adds rules to these painting checks. Early game items are less likely to appear into these paintings.
- Force Filler: For when you dislike doing these last two paintings. Their checks will only contain filler items."""
display_name = "Painting Checks Balancing"
option_none = 0
option_balanced = 1
option_force_filler = 2
default = 1
@dataclass
class InscryptionOptions(DeathLinkMixin, PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
act1_death_link_behaviour: Act1DeathLinkBehaviour
goal: Goal
randomize_codes: RandomizeCodes
randomize_deck: RandomizeDeck
randomize_sigils: RandomizeSigils
optional_death_card: OptionalDeathCard
skip_tutorial: SkipTutorial
skip_epilogue: SkipEpilogue
epitaph_pieces_randomization: EpitaphPiecesRandomization
painting_checks_balancing: PaintingChecksBalancing

View File

@@ -0,0 +1,14 @@
from typing import Dict, List
inscryption_regions_all: Dict[str, List[str]] = {
"Menu": ["Act 1", "Act 2", "Act 3", "Epilogue"],
"Act 1": [],
"Act 2": [],
"Act 3": [],
"Epilogue": []
}
inscryption_regions_act_1: Dict[str, List[str]] = {
"Menu": ["Act 1"],
"Act 1": []
}

181
worlds/inscryption/Rules.py Normal file
View File

@@ -0,0 +1,181 @@
from typing import Dict, Callable, TYPE_CHECKING
from BaseClasses import CollectionState, LocationProgressType
from .Options import Goal, PaintingChecksBalancing
if TYPE_CHECKING:
from . import InscryptionWorld
else:
InscryptionWorld = object
# Based on The Messenger's implementation
class InscryptionRules:
player: int
world: InscryptionWorld
location_rules: Dict[str, Callable[[CollectionState], bool]]
region_rules: Dict[str, Callable[[CollectionState], bool]]
def __init__(self, world: InscryptionWorld) -> None:
self.player = world.player
self.world = world
self.location_rules = {
"Act 1 - Wardrobe Drawer 1": self.has_wardrobe_key,
"Act 1 - Wardrobe Drawer 2": self.has_wardrobe_key,
"Act 1 - Wardrobe Drawer 3": self.has_wardrobe_key,
"Act 1 - Wardrobe Drawer 4": self.has_wardrobe_key,
"Act 1 - Dagger": self.has_caged_wolf,
"Act 1 - Magnificus Eye": self.has_dagger,
"Act 1 - Clock Main Compartment": self.has_magnificus_eye,
"Act 2 - Battle Prospector": self.has_camera_and_meat,
"Act 2 - Battle Angler": self.has_camera_and_meat,
"Act 2 - Battle Trapper": self.has_camera_and_meat,
"Act 2 - Battle Pike Mage": self.has_tower_requirements,
"Act 2 - Battle Goobert": self.has_tower_requirements,
"Act 2 - Battle Lonely Wizard": self.has_tower_requirements,
"Act 2 - Battle Inspector": self.has_act2_bridge_requirements,
"Act 2 - Battle Melter": self.has_act2_bridge_requirements,
"Act 2 - Battle Dredger": self.has_act2_bridge_requirements,
"Act 2 - Forest Meadow Chest": self.has_camera_and_meat,
"Act 2 - Tower Chest 1": self.has_act2_bridge_requirements,
"Act 2 - Tower Chest 2": self.has_tower_requirements,
"Act 2 - Tower Chest 3": self.has_tower_requirements,
"Act 2 - Tentacle": self.has_tower_requirements,
"Act 2 - Factory Trash Can": self.has_act2_bridge_requirements,
"Act 2 - Factory Drawer 1": self.has_act2_bridge_requirements,
"Act 2 - Factory Drawer 2": self.has_act2_bridge_requirements,
"Act 2 - Factory Chest 1": self.has_act2_bridge_requirements,
"Act 2 - Factory Chest 2": self.has_act2_bridge_requirements,
"Act 2 - Factory Chest 3": self.has_act2_bridge_requirements,
"Act 2 - Factory Chest 4": self.has_act2_bridge_requirements,
"Act 2 - Monocle": self.has_act2_bridge_requirements,
"Act 2 - Boss Grimora": self.has_all_epitaph_pieces,
"Act 2 - Boss Leshy": self.has_camera_and_meat,
"Act 2 - Boss Magnificus": self.has_tower_requirements,
"Act 2 - Boss P03": self.has_act2_bridge_requirements,
"Act 2 - Bone Lord Femur": self.has_obol,
"Act 2 - Bone Lord Horn": self.has_obol,
"Act 2 - Bone Lord Holo Key": self.has_obol,
"Act 2 - Mycologists Holo Key": self.has_tower_requirements, # Could need money
"Act 2 - Ancient Obol": self.has_tower_requirements, # Need money for the pieces? Use the tower mannequin.
"Act 3 - Boss Photographer": self.has_inspectometer_battery,
"Act 3 - Boss Archivist": self.has_battery_and_quill,
"Act 3 - Boss Unfinished": self.has_gems_and_battery,
"Act 3 - Boss G0lly": self.has_gems_and_battery,
"Act 3 - Extra Battery": self.has_inspectometer_battery, # Hard to miss but soft lock still possible.
"Act 3 - Nano Armor Generator": self.has_gems_and_battery, # Costs money, so can need multiple battles.
"Act 3 - Shop Holo Pelt": self.has_gems_and_battery, # Costs money, so can need multiple battles.
"Act 3 - Middle Holo Pelt": self.has_inspectometer_battery, # Can be reached without but possible soft lock
"Act 3 - Forest Holo Pelt": self.has_inspectometer_battery,
"Act 3 - Crypt Holo Pelt": self.has_inspectometer_battery,
"Act 3 - Tower Holo Pelt": self.has_gems_and_battery,
"Act 3 - Trader 1": self.has_pelts(1),
"Act 3 - Trader 2": self.has_pelts(2),
"Act 3 - Trader 3": self.has_pelts(3),
"Act 3 - Trader 4": self.has_pelts(4),
"Act 3 - Trader 5": self.has_pelts(5),
"Act 3 - Goobert's Painting": self.has_gems_and_battery,
"Act 3 - The Great Transcendence": self.has_transcendence_requirements,
"Act 3 - Boss Mycologists": self.has_mycologists_boss_requirements,
"Act 3 - Bone Lord Room": self.has_bone_lord_room_requirements,
"Act 3 - Luke's File Entry 1": self.has_battery_and_quill,
"Act 3 - Luke's File Entry 2": self.has_battery_and_quill,
"Act 3 - Luke's File Entry 3": self.has_battery_and_quill,
"Act 3 - Luke's File Entry 4": self.has_transcendence_requirements,
"Act 3 - Well": self.has_inspectometer_battery,
"Act 3 - Gems Drone": self.has_inspectometer_battery,
"Act 3 - Clock": self.has_gems_and_battery, # Can be brute-forced, but the solution needs those items.
}
self.region_rules = {
"Act 2": self.has_act2_requirements,
"Act 3": self.has_act3_requirements,
"Epilogue": self.has_epilogue_requirements
}
def has_wardrobe_key(self, state: CollectionState) -> bool:
return state.has("Wardrobe Key", self.player)
def has_caged_wolf(self, state: CollectionState) -> bool:
return state.has("Caged Wolf Card", self.player)
def has_dagger(self, state: CollectionState) -> bool:
return state.has("Dagger", self.player)
def has_magnificus_eye(self, state: CollectionState) -> bool:
return state.has("Magnificus Eye", self.player)
def has_useful_act1_items(self, state: CollectionState) -> bool:
return state.has_all(("Oil Painting's Clover Plant", "Squirrel Totem Head"), self.player)
def has_all_epitaph_pieces(self, state: CollectionState) -> bool:
return state.has(self.world.required_epitaph_pieces_name, self.player, self.world.required_epitaph_pieces_count)
def has_camera_and_meat(self, state: CollectionState) -> bool:
return state.has_all(("Camera Replica", "Pile Of Meat"), self.player)
def has_monocle(self, state: CollectionState) -> bool:
return state.has("Monocle", self.player)
def has_obol(self, state: CollectionState) -> bool:
return state.has("Ancient Obol", self.player)
def has_epitaphs_and_forest_items(self, state: CollectionState) -> bool:
return self.has_camera_and_meat(state) and self.has_all_epitaph_pieces(state)
def has_act2_bridge_requirements(self, state: CollectionState) -> bool:
return self.has_camera_and_meat(state) or self.has_all_epitaph_pieces(state)
def has_tower_requirements(self, state: CollectionState) -> bool:
return self.has_monocle(state) and self.has_act2_bridge_requirements(state)
def has_inspectometer_battery(self, state: CollectionState) -> bool:
return state.has("Inspectometer Battery", self.player)
def has_gems_and_battery(self, state: CollectionState) -> bool:
return state.has("Gems Module", self.player) and self.has_inspectometer_battery(state)
def has_pelts(self, count: int) -> Callable[[CollectionState], bool]:
return lambda state: state.has("Holo Pelt", self.player, count) and self.has_gems_and_battery(state)
def has_mycologists_boss_requirements(self, state: CollectionState) -> bool:
return state.has("Mycologists Holo Key", self.player) and self.has_transcendence_requirements(state)
def has_bone_lord_room_requirements(self, state: CollectionState) -> bool:
return state.has("Bone Lord Holo Key", self.player) and self.has_inspectometer_battery(state)
def has_battery_and_quill(self, state: CollectionState) -> bool:
return state.has("Quill", self.player) and self.has_inspectometer_battery(state)
def has_transcendence_requirements(self, state: CollectionState) -> bool:
return state.has("Quill", self.player) and self.has_gems_and_battery(state)
def has_act2_requirements(self, state: CollectionState) -> bool:
return state.has("Film Roll", self.player)
def has_act3_requirements(self, state: CollectionState) -> bool:
return self.has_act2_requirements(state) and self.has_all_epitaph_pieces(state) and \
self.has_camera_and_meat(state) and self.has_monocle(state)
def has_epilogue_requirements(self, state: CollectionState) -> bool:
return self.has_act3_requirements(state) and self.has_transcendence_requirements(state)
def set_all_rules(self) -> None:
multiworld = self.world.multiworld
if self.world.options.goal != Goal.option_first_act:
multiworld.completion_condition[self.player] = self.has_epilogue_requirements
else:
multiworld.completion_condition[self.player] = self.has_act2_requirements
for region in multiworld.get_regions(self.player):
if self.world.options.goal == Goal.option_full_story_in_order:
if region.name in self.region_rules:
for entrance in region.entrances:
entrance.access_rule = self.region_rules[region.name]
for loc in region.locations:
if loc.name in self.location_rules:
loc.access_rule = self.location_rules[loc.name]
if self.world.options.painting_checks_balancing == PaintingChecksBalancing.option_balanced:
self.world.get_location("Act 1 - Painting 2").access_rule = self.has_useful_act1_items
self.world.get_location("Act 1 - Painting 3").access_rule = self.has_useful_act1_items
elif self.world.options.painting_checks_balancing == PaintingChecksBalancing.option_force_filler:
self.world.get_location("Act 1 - Painting 2").progress_type = LocationProgressType.EXCLUDED
self.world.get_location("Act 1 - Painting 3").progress_type = LocationProgressType.EXCLUDED

View File

@@ -0,0 +1,144 @@
from .Options import InscryptionOptions, Goal, EpitaphPiecesRandomization, PaintingChecksBalancing
from .Items import act1_items, act2_items, act3_items, filler_items, base_id, InscryptionItem, ItemDict
from .Locations import act1_locations, act2_locations, act3_locations, regions_to_locations
from .Regions import inscryption_regions_all, inscryption_regions_act_1
from typing import Dict, Any
from . import Rules
from BaseClasses import Region, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
class InscrypWeb(WebWorld):
theme = "dirt"
guide_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Inscryption Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["DrBibop"]
)
guide_fr = Tutorial(
"Multiworld Setup Guide",
"Un guide pour configurer Inscryption Archipelago Multiworld",
"Français",
"setup_fr.md",
"setup/fr",
["Glowbuzz"]
)
tutorials = [guide_en, guide_fr]
bug_report_page = "https://github.com/DrBibop/Archipelago_Inscryption/issues"
class InscryptionWorld(World):
"""
Inscryption is an inky black card-based odyssey that blends the deckbuilding roguelike,
escape-room style puzzles, and psychological horror into a blood-laced smoothie.
Darker still are the secrets inscrybed upon the cards...
"""
game = "Inscryption"
web = InscrypWeb()
options_dataclass = InscryptionOptions
options: InscryptionOptions
all_items = act1_items + act2_items + act3_items + filler_items
item_name_to_id = {item["name"]: i + base_id for i, item in enumerate(all_items)}
all_locations = act1_locations + act2_locations + act3_locations
location_name_to_id = {location: i + base_id for i, location in enumerate(all_locations)}
required_epitaph_pieces_count = 9
required_epitaph_pieces_name = "Epitaph Piece"
def generate_early(self) -> None:
self.all_items = [item.copy() for item in self.all_items]
if self.options.epitaph_pieces_randomization == EpitaphPiecesRandomization.option_all_pieces:
self.required_epitaph_pieces_name = "Epitaph Piece"
self.required_epitaph_pieces_count = 9
elif self.options.epitaph_pieces_randomization == EpitaphPiecesRandomization.option_in_groups:
self.required_epitaph_pieces_name = "Epitaph Pieces"
self.required_epitaph_pieces_count = 3
else:
self.required_epitaph_pieces_name = "Epitaph Pieces"
self.required_epitaph_pieces_count = 1
if self.options.painting_checks_balancing == PaintingChecksBalancing.option_balanced:
self.all_items[6]["classification"] = ItemClassification.progression
self.all_items[11]["classification"] = ItemClassification.progression
if self.options.painting_checks_balancing == PaintingChecksBalancing.option_force_filler \
and self.options.goal == Goal.option_first_act:
self.all_items[3]["classification"] = ItemClassification.filler
if self.options.epitaph_pieces_randomization != EpitaphPiecesRandomization.option_all_pieces:
self.all_items[len(act1_items) + 3]["count"] = self.required_epitaph_pieces_count
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)["name"]
def create_item(self, name: str) -> Item:
item_id = self.item_name_to_id[name]
item_data = self.all_items[item_id - base_id]
return InscryptionItem(name, item_data["classification"], item_id, self.player)
def create_items(self) -> None:
nb_items_added = 0
useful_items = self.all_items.copy()
if self.options.goal != Goal.option_first_act:
useful_items = [item for item in useful_items
if not any(filler_item["name"] == item["name"] for filler_item in filler_items)]
if self.options.epitaph_pieces_randomization == EpitaphPiecesRandomization.option_all_pieces:
useful_items.pop(len(act1_items) + 3)
else:
useful_items.pop(len(act1_items) + 2)
else:
useful_items = [item for item in useful_items
if any(act1_item["name"] == item["name"] for act1_item in act1_items)]
for item in useful_items:
for _ in range(item["count"]):
new_item = self.create_item(item["name"])
self.multiworld.itempool.append(new_item)
nb_items_added += 1
filler_count = len(self.all_locations if self.options.goal != Goal.option_first_act else act1_locations)
filler_count -= nb_items_added
for i in range(filler_count):
index = i % len(filler_items)
filler_item = filler_items[index]
new_item = self.create_item(filler_item["name"])
self.multiworld.itempool.append(new_item)
def create_regions(self) -> None:
used_regions = inscryption_regions_all if self.options.goal != Goal.option_first_act \
else inscryption_regions_act_1
for region_name in used_regions.keys():
self.multiworld.regions.append(Region(region_name, self.player, self.multiworld))
for region_name, region_connections in used_regions.items():
region = self.get_region(region_name)
region.add_exits(region_connections)
region.add_locations({
location: self.location_name_to_id[location] for location in regions_to_locations[region_name]
})
def set_rules(self) -> None:
Rules.InscryptionRules(self).set_all_rules()
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict(
"death_link",
"act1_death_link_behaviour",
"goal",
"randomize_codes",
"randomize_deck",
"randomize_sigils",
"optional_death_card",
"skip_tutorial",
"skip_epilogue",
"epitaph_pieces_randomization"
)

View File

@@ -0,0 +1,22 @@
# Inscryption
## Where is the options page?
You can configure your player options with the Inscryption options page. [Click here](../player-options) to start configuring them to your liking.
## What does randomization do to this game?
Due to the nature of the randomizer, you are allowed to return to a previous act you've previously completed if there are location checks you've missed. The "New Game" option is replaced with a "Chapter Select" option and is enabled after you beat act 1. If you prefer, you can also make all acts available from the start by changing the goal option. All items that you can find lying around, in containers, or from puzzles are randomized and replaced with location checks. Boss fights from all acts and battles from act 2 also count as location checks.
## What is the goal of Inscryption when randomized?
By default, the goal is considered reached once you open the OLD_DATA file. This means playing through all three acts in order and the epilogue. You can change the goal option to instead complete all acts in any order or simply complete act 1.
## Which items can be in another player's world?
All key items necessary for progression such as the film roll, the dagger, Grimora's epitaphs, etc. Unique cards that aren't randomly found in the base game (e.g. talking cards) are also included. For filler items, you can receive currency which will be added to every act's bank or card packs that you can open at any time when inspecting your deck.
## What does another world's item look like in Inscryption?
Items from other worlds usually take the appearance of a normal card from the current act you're playing. The card's name contains the item that will be sent when picked up and its portrait is the Archipelago logo (a ring of six circles). Picking up these cards does not add them to your deck.
## When the player receives an item, what happens?
The item is instantly granted to you. A yellow message appears in the Archipelago logs at the top-right of your screen. An audio cue is also played. If the item received is a holdable item (wardrobe key, inspectometer battery, gems module), the item will be placed where you would usually collect it in a vanilla playthrough (safe, inspectometer, drone).
## How many items can I find or receive in my world?
By default, if all three acts are played, there are **100** randomized locations in your world and **100** of your items shuffled in the multiworld. There are **17** locations in act 1 (this will be the total amount if you decide to only play act 1), **52** locations in act 2, and **31** locations in act 3.

View File

@@ -0,0 +1,65 @@
# Inscryption Randomizer Setup Guide
## Required Software
- [Inscryption](https://store.steampowered.com/app/1092790/Inscryption/)
- For easy setup (recommended):
- [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) OR [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager)
- For manual setup:
- [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/)
- [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/)
## Installation
Before starting the installation process, here's what you should know:
- Only install the mods mentioned in this guide if you want a guaranteed smooth experience! Other mods were NOT tested with ArchipelagoMod and could cause unwanted issues.
- The ArchipelagoMod uses its own save file system when playing, but for safety measures, back up your save file by going to your Inscryption installation directory and copy the `SaveFile.gwsave` file to another folder.
- It is strongly recommended to use a mod manager if you want a quicker and easier installation process, but if you don't like installing extra software and are comfortable moving files around, you can refer to the manual setup guide instead.
### Easy setup (mod manager)
1. Download [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) using the "Manual Download" button, then install it using the executable in the downloaded zip package (You can also use [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager) which works the same, but it requires [Overwolf](https://www.overwolf.com/))
2. Open the mod manager and select Inscryption in the game selection screen.
3. Select the default profile or create a new one.
4. Open the `Online` tab on the left, then search for `ArchipelagoMod`.
5. Expand ArchipelagoMod and click the `Download` button to install the latest version and all its dependencies.
6. Click `Start Modded` to open the game with the mods (a console should appear if everything was done correctly).
### Manual setup
1. Download the following mods using the `Manual Download` button:
- [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/)
- [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/)
2. Open your Inscryption installation directory. On Steam, you can find it easily by right-clicking the game and clicking `Manage` > `Browse local files`.
3. Open the BepInEx pack zip file, then open the `BepInExPack_Inscryption` folder.
4. Drag all folders and files located inside the `BepInExPack_Inscryption` folder and drop them in your Inscryption directory.
5. Open the `BepInEx` folder in your Inscryption directory.
6. Open the ArchipelagoMod zip file.
7. Drag and drop the `plugins` folder in the `BepInEx` folder to fuse with the existing `plugins` folder.
8. Open the game normally to play with mods (if BepInEx was installed correctly, a console should appear).
## Joining a new MultiWorld Game
1. After opening the game, you should see a new menu for browsing and creating save files.
2. Click on the `New Game` button, then write a unique name for your save file.
3. On the next screen, enter the information needed to connect to the MultiWorld server, then press the `Connect` button.
4. If successful, the status on the top-right will change to "Connected". If not, a red error message will appear.
5. After connecting to the server and receiving items, the game menu will appear.
## Continuing a MultiWorld Game
1. After opening the game, you should see a list of your save files and a button to add a new one.
2. Find the save file you want to use, then click its `Play` button.
3. On the next screen, the input fields will be filled with the information you've written previously. You can adjust some fields if needed, then press the `Connect` button.
4. If successful, the status on the top-right will change to "connected". If not, a red error message will appear.
5. After connecting to the server and receiving items, the game menu will appear.
## Troubleshooting
### The game opens normally without the new menu.
If the new menu mentioned previously doesn't appear, it can be one of two issues:
- If there was no console appearing when opening the game, this means the mods didn't load correctly. Here's what you can try:
- If you are using the mod manager, make sure to open it and press `Start Modded`. Opening the game normally from Steam won't load any mods.
- Check if the mod manager correctly found the game path. In the mod manager, click `Settings` then go to the `Locations` tab. Make sure the path listed under `Change Inscryption directory` is correct. You can verify the real path if you right-click the game on steam and click `Manage` > `Browse local files`. If the path is wrong, click that setting and change the path.
- If you installed the mods manually, this usually means BepInEx was not correctly installed. Make sure to read the installation guide carefully.
- If there is still no console when opening the game modded, try asking in the [Archipelago Discord Server](https://discord.gg/8Z65BR2) for help.
- If there is a console, this means the mods loaded but the ArchipelagoMod wasn't found or had errors while loading.
- Look in the console and make sure you can find a message about ArchipelagoMod being loaded.
- If you see any red text, there was an error. Report the issue in the [Archipelago Discord Server](https://discord.gg/8Z65BR2) or create an issue in our [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues).
### I'm getting a different issue.
You can ask for help in the [Archipelago Discord Server](https://discord.gg/8Z65BR2) or, if you think you've found a bug with the mod, create an issue in our [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues).

View File

@@ -0,0 +1,67 @@
# Guide d'Installation de Inscryption Randomizer
## Logiciel Exigé
- [Inscryption](https://store.steampowered.com/app/1092790/Inscryption/)
- Pour une installation facile (recommandé):
- [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) OU [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager)
- Pour une installation manuelle:
- [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/)
- [MonoMod Loader for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/MonoMod_Loader_Inscryption/)
- [Inscryption API](https://inscryption.thunderstore.io/package/API_dev/API/)
- [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/)
## Installation
Avant de commencer le processus d'installation, voici ce que vous deviez savoir:
- Installez uniquement les mods mentionnés dans ce guide si vous souhaitez une expérience stable! Les autres mods n'ont PAS été testés avec ArchipelagoMod et peuvent provoquer des problèmes.
- ArchipelagoMod utilise son propre système de sauvegarde lorsque vous jouez, mais pour des raisons de sécurité, sauvegardez votre fichier de sauvegarde en accédant à votre répertoire d'installation Inscryption et copiez le fichier `SaveFile.gwsave` dans un autre dossier.
- Il est fortement recommandé d'utiliser un mod manager si vous souhaitez avoir un processus d'installation plus rapide et plus facile, mais si vous n'aimez pas installer de logiciels supplémentaires et que vous êtes à l'aise pour déplacer des fichiers, vous pouvez vous référer au guide de configuration manuelle.
### Installation facile (mod manager)
1. Téléchargez [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) à l'aide du bouton `Manual Download`, puis installez-le à l'aide de l'exécutable contenu dans le zip téléchargé (vous pouvez également utiliser [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager) qui fonctionne de la même manière, mais cela nécessite [Overwolf](https://www.overwolf.com/))
2. Ouvrez le mod manager et sélectionnez Inscryption dans l'écran de sélection de jeu.
3. Sélectionnez le profil par défaut ou créez-en un nouveau.
4. Ouvrez l'onglet `Online` à gauche, puis recherchez `ArchipelagoMod`.
5. Développez ArchipelagoMod et cliquez sur le bouton `Download` pour installer la dernière version disponible et toutes ses dépendances.
6. Cliquez sur `Start Modded` pour ouvrir le jeu avec les mods (une console devrait apparaître si tout a été fait correctement).
### Installation manuelle
1. Téléchargez les mods suivants en utilisant le bouton `Manual Download`:
- [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/)
- [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/)
2. Ouvrez votre dossier d'installation d'Inscryption. Sur Steam, vous pouvez le trouver facilement en faisant un clic droit sur le jeu et en cliquant sur `Gérer` > `Parcourir les fichiers locaux`.
3. Ouvrez le fichier zip du pack BepInEx, puis ouvrez le dossier `BepInExPack_Inscryption`.
4. Prenez tous les dossiers et fichiers situés dans le dossier `BepInExPack_Inscryption` et déposez-les dans votre dossier Inscryption.
5. Ouvrez le dossier `BepInEx` dans votre dossier Inscryption.
6. Ouvrez le fichier zip d'ArchipelagoMod.
7. Prenez et déposez le dossier `plugins` dans le dossier `BepInEx` pour fusionner avec le dossier `plugins` existant.
8. Ouvrez le jeu normalement pour jouer avec les mods (si BepInEx a été correctement installé, une console devrait apparaitre).
## Rejoindre un nouveau MultiWorld
1. Après avoir ouvert le jeu, vous devriez voir un nouveau menu pour parcourir et créer des fichiers de sauvegarde.
2. Cliquez sur le bouton `New Game`, puis écrivez un nom unique pour votre fichier de sauvegarde.
3. Sur l'écran suivant, saisissez les informations nécessaires pour vous connecter au serveur MultiWorld, puis appuyez sur le bouton `Connect`.
4. En cas de succès, l'état de connexion en haut à droite changera pour "Connected". Sinon, un message d'erreur rouge apparaîtra.
5. Après s'être connecté au server et avoir reçu les items, le menu du jeu apparaîtra.
## Poursuivre une session MultiWorld
1. Après avoir ouvert le jeu, vous devriez voir une liste de vos fichiers de sauvegarde et un bouton pour en ajouter un nouveau.
2. Choisissez le fichier de sauvegarde que vous souhaitez utiliser, puis cliquez sur son bouton `Play`.
3. Sur l'écran suivant, les champs de texte seront remplis avec les informations que vous avez écrites précédemment. Vous pouvez ajuster certains champs si nécessaire, puis appuyer sur le bouton `Connect`.
4. En cas de succès, l'état de connexion en haut à droite changera pour "Connected". Sinon, un message d'erreur rouge apparaîtra.
5. Après s'être connecté au server et avoir reçu les items, le menu du jeu apparaîtra.
## Dépannage
### Le jeu ouvre normalement sans nouveau menu.
Si le nouveau menu mentionné précédemment n'apparaît pas, c'est peut-être l'un des deux problèmes suivants:
- Si aucune console n'apparait à l'ouverture du jeu, cela signifie que les mods ne se sont pas chargés correctement. Voici ce que vous pouvez essayer:
- Si vous utilisez le mod manager, assurez-vous de l'ouvrir et d'appuyer sur `Start Modded`. Ouvrir le jeu normalement depuis Steam ne chargera aucun mod.
- Vérifiez si le mod manager a correctement trouvé le répertoire du jeu. Dans le mod manager, cliquez sur `Settings` puis allez dans l'onglet `Locations`. Assurez-vous que le répertoire sous `Change Inscryption directory` est correct. Vous pouvez vérifier le répertoire correct si vous faites un clic droit sur le jeu Inscription sur Steam et cliquez sur `Gérer` > `Parcourir les fichiers locaux`. Si le répertoire est erroné, cliquez sur ce paramètre et modifiez le répertoire.
- Si vous avez installé les mods manuellement, cela signifie généralement que BepInEx n'a pas été correctement installé. Assurez-vous de lire attentivement le guide d'installation.
- S'il n'y a toujours pas de console lors de l'ouverture du jeu modifié, essayez de demander de l'aide sur [Archipelago Discord Server](https://discord.gg/8Z65BR2).
- S'il y a une console, cela signifie que les mods ont été chargés, mais que ArchipelagoMod n'a pas été trouvé ou a eu des erreurs lors du chargement.
- Regardez dans la console et assurez-vous que vous trouvez un message concernant le chargement d'ArchipelagoMod.
- Si vous voyez du texte rouge, il y a eu une erreur. Signalez le problème dans [Archipelago Discord Server](https://discord.gg/8Z65BR2) ou dans notre [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues).
### J'ai un autre problème.
Vous pouvez demander de l'aide sur [le serveur Discord d'Archipelago](https://discord.gg/8Z65BR2) ou, si vous pensez avoir trouvé un bug avec le mod, signalez-le dans notre [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues).

View File

@@ -0,0 +1,221 @@
from . import InscryptionTestBase
class AccessTestGeneral(InscryptionTestBase):
def test_dagger(self) -> None:
self.assertAccessDependency(["Act 1 - Magnificus Eye"], [["Dagger"]])
def test_caged_wolf(self) -> None:
self.assertAccessDependency(["Act 1 - Dagger"], [["Caged Wolf Card"]])
def test_magnificus_eye(self) -> None:
self.assertAccessDependency(["Act 1 - Clock Main Compartment"], [["Magnificus Eye"]])
def test_wardrobe_key(self) -> None:
self.assertAccessDependency(
["Act 1 - Wardrobe Drawer 1", "Act 1 - Wardrobe Drawer 2",
"Act 1 - Wardrobe Drawer 3", "Act 1 - Wardrobe Drawer 4"],
[["Wardrobe Key"]]
)
def test_ancient_obol(self) -> None:
self.assertAccessDependency(
["Act 2 - Bone Lord Femur", "Act 2 - Bone Lord Horn", "Act 2 - Bone Lord Holo Key"],
[["Ancient Obol"]]
)
def test_holo_pelt(self) -> None:
self.assertAccessDependency(
["Act 3 - Trader 1", "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5"],
[["Holo Pelt"]]
)
def test_inspectometer_battery(self) -> None:
self.assertAccessDependency(
["Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly",
"Act 3 - Trader 1", "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5",
"Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", "Act 3 - Forest Holo Pelt", "Act 3 - Clock",
"Act 3 - Crypt Holo Pelt", "Act 3 - Gems Drone", "Act 3 - Nano Armor Generator", "Act 3 - Extra Battery",
"Act 3 - Tower Holo Pelt", "Act 3 - The Great Transcendence", "Act 3 - Boss Mycologists",
"Act 3 - Bone Lord Room", "Act 3 - Well", "Act 3 - Luke's File Entry 1", "Act 3 - Luke's File Entry 2",
"Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", "Act 3 - Goobert's Painting"],
[["Inspectometer Battery"]]
)
def test_gem_drone(self) -> None:
self.assertAccessDependency(
["Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", "Act 3 - Trader 1", "Act 3 - Trader 2",
"Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Shop Holo Pelt", "Act 3 - Clock",
"Act 3 - Tower Holo Pelt", "Act 3 - The Great Transcendence", "Act 3 - Luke's File Entry 4",
"Act 3 - Boss Mycologists", "Act 3 - Nano Armor Generator", "Act 3 - Goobert's Painting"],
[["Gems Module"]]
)
def test_mycologists_holo_key(self) -> None:
self.assertAccessDependency(
["Act 3 - Boss Mycologists"],
[["Mycologists Holo Key"]]
)
def test_bone_lord_holo_key(self) -> None:
self.assertAccessDependency(
["Act 3 - Bone Lord Room"],
[["Bone Lord Holo Key"]]
)
def test_quill(self) -> None:
self.assertAccessDependency(
["Act 3 - Boss Archivist", "Act 3 - Luke's File Entry 1", "Act 3 - Luke's File Entry 2",
"Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", "Act 3 - The Great Transcendence",
"Act 3 - Boss Mycologists"],
[["Quill"]]
)
class AccessTestOrdered(InscryptionTestBase):
options = {
"goal": 0,
}
def test_film_roll(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper", "Act 2 - Battle Sawyer",
"Act 2 - Battle Royal", "Act 2 - Battle Kaycee", "Act 2 - Battle Pike Mage", "Act 2 - Battle Goobert",
"Act 2 - Battle Lonely Wizard", "Act 2 - Battle Inspector", "Act 2 - Battle Melter",
"Act 2 - Battle Dredger", "Act 2 - Tower Chest 1", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3",
"Act 2 - Forest Meadow Chest", "Act 2 - Forest Cabin Chest", "Act 2 - Cabin Wardrobe Drawer",
"Act 2 - Cabin Safe", "Act 2 - Crypt Casket 1", "Act 2 - Crypt Casket 2", "Act 2 - Crypt Well",
"Act 2 - Camera Replica", "Act 2 - Clover", "Act 2 - Epitaph Piece 1", "Act 2 - Epitaph Piece 2",
"Act 2 - Epitaph Piece 3", "Act 2 - Epitaph Piece 4", "Act 2 - Epitaph Piece 5", "Act 2 - Epitaph Piece 6",
"Act 2 - Epitaph Piece 7", "Act 2 - Epitaph Piece 8", "Act 2 - Epitaph Piece 9", "Act 2 - Dock Chest",
"Act 2 - Tentacle", "Act 2 - Factory Trash Can", "Act 2 - Factory Drawer 1",
"Act 2 - Ancient Obol", "Act 2 - Factory Drawer 2", "Act 2 - Factory Chest 1", "Act 2 - Factory Chest 2",
"Act 2 - Factory Chest 3", "Act 2 - Factory Chest 4", "Act 2 - Monocle", "Act 2 - Boss Leshy",
"Act 2 - Boss Grimora", "Act 2 - Boss Magnificus", "Act 2 - Boss P03", "Act 2 - Mycologists Holo Key",
"Act 2 - Bone Lord Femur", "Act 2 - Bone Lord Horn", "Act 2 - Bone Lord Holo Key",
"Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly",
"Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt",
"Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1",
"Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1",
"Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator",
"Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone",
"Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4",
"Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"],
[["Film Roll"]]
)
def test_epitaphs_and_forest_items(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper",
"Act 2 - Battle Pike Mage", "Act 2 - Battle Goobert", "Act 2 - Battle Lonely Wizard",
"Act 2 - Battle Inspector", "Act 2 - Battle Melter", "Act 2 - Battle Dredger",
"Act 2 - Tower Chest 1", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", "Act 2 - Forest Meadow Chest",
"Act 2 - Tentacle", "Act 2 - Factory Trash Can", "Act 2 - Factory Drawer 1", "Act 2 - Ancient Obol",
"Act 2 - Factory Drawer 2", "Act 2 - Factory Chest 1", "Act 2 - Factory Chest 2",
"Act 2 - Factory Chest 3", "Act 2 - Factory Chest 4", "Act 2 - Monocle", "Act 2 - Boss Leshy",
"Act 2 - Boss Grimora", "Act 2 - Boss Magnificus", "Act 2 - Boss P03", "Act 2 - Mycologists Holo Key",
"Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly",
"Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt",
"Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1",
"Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1",
"Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator",
"Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone",
"Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4",
"Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"],
[["Epitaph Piece", "Camera Replica", "Pile Of Meat"]]
)
def test_epitaphs(self) -> None:
self.assertAccessDependency(
["Act 2 - Boss Grimora",
"Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly",
"Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt",
"Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1",
"Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1",
"Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator",
"Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone",
"Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4",
"Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"],
[["Epitaph Piece"]]
)
def test_forest_items(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper",
"Act 2 - Boss Leshy", "Act 2 - Forest Meadow Chest",
"Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly",
"Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt",
"Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1",
"Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1",
"Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator",
"Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone",
"Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4",
"Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"],
[["Camera Replica", "Pile Of Meat"]]
)
def test_monocle(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Goobert", "Act 2 - Battle Pike Mage", "Act 2 - Battle Lonely Wizard",
"Act 2 - Boss Magnificus", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3",
"Act 2 - Tentacle", "Act 2 - Ancient Obol", "Act 2 - Mycologists Holo Key",
"Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly",
"Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt",
"Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1",
"Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1",
"Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator",
"Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone",
"Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4",
"Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"],
[["Monocle"]]
)
class AccessTestUnordered(InscryptionTestBase):
options = {
"goal": 1,
}
def test_epitaphs_and_forest_items(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper",
"Act 2 - Battle Pike Mage", "Act 2 - Battle Goobert", "Act 2 - Battle Lonely Wizard",
"Act 2 - Battle Inspector", "Act 2 - Battle Melter", "Act 2 - Battle Dredger",
"Act 2 - Tower Chest 1", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", "Act 2 - Forest Meadow Chest",
"Act 2 - Tentacle", "Act 2 - Factory Trash Can", "Act 2 - Factory Drawer 1", "Act 2 - Ancient Obol",
"Act 2 - Factory Drawer 2", "Act 2 - Factory Chest 1", "Act 2 - Factory Chest 2",
"Act 2 - Factory Chest 3", "Act 2 - Factory Chest 4", "Act 2 - Monocle", "Act 2 - Boss Leshy",
"Act 2 - Boss Grimora", "Act 2 - Boss Magnificus", "Act 2 - Boss P03", "Act 2 - Mycologists Holo Key"],
[["Epitaph Piece", "Camera Replica", "Pile Of Meat"]]
)
def test_epitaphs(self) -> None:
self.assertAccessDependency(
["Act 2 - Boss Grimora"],
[["Epitaph Piece"]]
)
def test_forest_items(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper",
"Act 2 - Boss Leshy", "Act 2 - Forest Meadow Chest"],
[["Camera Replica", "Pile Of Meat"]]
)
def test_monocle(self) -> None:
self.assertAccessDependency(
["Act 2 - Battle Goobert", "Act 2 - Battle Pike Mage", "Act 2 - Battle Lonely Wizard",
"Act 2 - Boss Magnificus", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3",
"Act 2 - Tentacle", "Act 2 - Ancient Obol", "Act 2 - Mycologists Holo Key"],
[["Monocle"]]
)
class AccessTestBalancedPaintings(InscryptionTestBase):
options = {
"painting_checks_balancing": 1,
}
def test_paintings(self) -> None:
self.assertAccessDependency(["Act 1 - Painting 2", "Act 1 - Painting 3"],
[["Oil Painting's Clover Plant", "Squirrel Totem Head"]])

View File

@@ -0,0 +1,108 @@
from . import InscryptionTestBase
class GoalTestOrdered(InscryptionTestBase):
options = {
"goal": 0,
}
def test_beatable(self) -> None:
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.collect(item)
for i in range(9):
item = self.get_item_by_name("Epitaph Piece")
self.collect(item)
self.assertBeatable(True)
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.remove(item)
self.assertBeatable(False)
self.collect(item)
item = self.get_item_by_name("Epitaph Piece")
self.remove(item)
self.assertBeatable(False)
self.collect(item)
class GoalTestUnordered(InscryptionTestBase):
options = {
"goal": 1,
}
def test_beatable(self) -> None:
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.collect(item)
for i in range(9):
item = self.get_item_by_name("Epitaph Piece")
self.collect(item)
self.assertBeatable(True)
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.remove(item)
self.assertBeatable(False)
self.collect(item)
item = self.get_item_by_name("Epitaph Piece")
self.remove(item)
self.assertBeatable(False)
self.collect(item)
class GoalTestAct1(InscryptionTestBase):
options = {
"goal": 2,
}
def test_beatable(self) -> None:
self.assertBeatable(False)
film_roll = self.get_item_by_name("Film Roll")
self.collect(film_roll)
self.assertBeatable(True)
class GoalTestGroupedEpitaphs(InscryptionTestBase):
options = {
"epitaph_pieces_randomization": 1,
}
def test_beatable(self) -> None:
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.collect(item)
for i in range(3):
item = self.get_item_by_name("Epitaph Pieces")
self.collect(item)
self.assertBeatable(True)
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.remove(item)
self.assertBeatable(False)
self.collect(item)
item = self.get_item_by_name("Epitaph Pieces")
self.remove(item)
self.assertBeatable(False)
self.collect(item)
class GoalTestEpitaphsAsOne(InscryptionTestBase):
options = {
"epitaph_pieces_randomization": 2,
}
def test_beatable(self) -> None:
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.collect(item)
item = self.get_item_by_name("Epitaph Pieces")
self.collect(item)
self.assertBeatable(True)
for item_name in self.required_items_all_acts:
item = self.get_item_by_name(item_name)
self.remove(item)
self.assertBeatable(False)
self.collect(item)
item = self.get_item_by_name("Epitaph Pieces")
self.remove(item)
self.assertBeatable(False)
self.collect(item)

View File

@@ -0,0 +1,7 @@
from test.bases import WorldTestBase
class InscryptionTestBase(WorldTestBase):
game = "Inscryption"
required_items_all_acts = ["Film Roll", "Camera Replica", "Pile Of Meat", "Monocle",
"Inspectometer Battery", "Gems Module", "Quill"]

View File

@@ -2,7 +2,7 @@ import logging
from typing import List
from BaseClasses import Tutorial, ItemClassification
from Fill import fill_restrictive
from Fill import fast_fill
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.AutoWorld import World, WebWorld
from .Items import *
@@ -287,7 +287,7 @@ class KH2World(World):
def pre_fill(self):
"""
Plandoing Events and Fill_Restrictive for donald,goofy and sora
Plandoing Events and Fast_Fill for donald,goofy and sora
"""
self.donald_pre_fill()
self.goofy_pre_fill()
@@ -431,9 +431,10 @@ class KH2World(World):
Fills keyblade slots with abilities determined on player's setting
"""
keyblade_locations = [self.multiworld.get_location(location, self.player) for location in Keyblade_Slots.keys()]
state = self.multiworld.get_all_state(False)
keyblade_ability_pool_copy = self.keyblade_ability_pool.copy()
fill_restrictive(self.multiworld, state, keyblade_locations, keyblade_ability_pool_copy, True, True, allow_excluded=True)
fast_fill(self.multiworld, keyblade_ability_pool_copy, keyblade_locations)
for location in keyblade_locations:
location.locked = True
def starting_invo_verify(self):
"""

View File

@@ -7,18 +7,21 @@
<h2 style="text-transform:none";>Required Software:</h2>
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea`
2. `Lua Backend from the OpenKH Mod Manager`
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
5. `AP Randomizer Seed`
1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
3. Install the mod from `KH2FM-Mods-equations19/auto-save` using OpenKH Mod Manager
4. Install the mod from `KH2FM-Mods-equations19/KH2-Lua-Library` using OpenKH Mod Manager
5. AP Randomizer Seed
- Optional Quality of Life Mods for Archipelago
1. Optionally Install the Archipelago Quality Of Life mod from `JaredWeakStrike/AP_QOL` using OpenKH Mod Manager
2. Optionally Install the Quality Of Life mod from `shananas/BearSkip` using OpenKH Mod Manager
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
@@ -26,15 +29,21 @@ Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `
Have this mod second-highest priority below the .zip seed.<br>
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
<h3 style="text-transform:none";>Required: Auto Save Mod</h3>
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
<h3 style="text-transform:none";>Installing A Seed</h3>
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`.<br>
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and "Select and install Mod Archive".<br>
Make sure the seed is on the top of the list (Highest Priority)<br>
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
<h2 style="text-transform:none";>Optional Software:</h2>
@@ -48,18 +57,21 @@ After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot
<h2 style="text-transform:none";>Using the KH2 Client</h2>
Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.<br>
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`<br>
Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.<br>
Most checks will be sent to you anywhere outside a load or cutscene.<br>
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.`
If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
<h2 style="text-transform:none";>KH2 Client should look like this: </h2>
![image](https://i.imgur.com/qP6CmV8.png)
Enter `The room's port number` into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
Enter The room's port number into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
<h2 style="text-transform:none";>Common Pitfalls</h2>
@@ -102,7 +114,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
- Why is my Client giving me a "Cannot Open Process: " error?
- Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin.
- Why is my HP/MP continuously increasing without stopping?
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager.
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the GoA ROM Edition Mod in the mod manager.
- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod?
- You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`.
- Why am I missing worlds/portals in the GoA?
@@ -110,9 +122,9 @@ This pack will handle logic, received items, checked locations and autotabbing f
- Why did I not load into the correct visit?
- You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item.
- What versions of Kingdom Hearts 2 are supported?
- Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`.
- Currently the only supported versions are Epic Games Version 1.0.0.10_WW and Steam Build Version 15194255.
- Why am I getting wallpapered while going into a world for the first time?
- Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
- Your Lua Backend was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
- Why am I not getting magic?
- If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
- Why did I crash after picking my dream weapon?
@@ -124,7 +136,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
- You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up)
- Why am I not sending or receiving items?
- Make sure you are connected to the KH2 client and the correct room (for more information scroll up)
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`?
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress.
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
- How do I load an auto save?
- To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time.

View File

@@ -0,0 +1,531 @@
BLOCKED_ASSOCIATIONS = [
# MAX_ARROWS_UPGRADE, MAX_BOMBS_UPGRADE, MAX_POWDER_UPGRADE
# arrows and bombs will be matched to arrow and bomb respectively through pluralization
"ARROWS",
"BOMBS",
"MAX",
"UPGRADE",
"TAIL", # TAIL_KEY
"ANGLER", # ANGLER_KEY
"FACE", # FACE_KEY
"BIRD", # BIRD_KEY
"SLIME", # SLIME_KEY
"NIGHTMARE",# NIGHTMARE_KEY
"BLUE", # BLUE_TUNIC
"RED", # RED_TUNIC
"TRADING", # TRADING_ITEM_*
"ITEM", # TRADING_ITEM_*
"BAD", # BAD_HEART_CONTAINER
"GOLD", # GOLD_LEAF
"MAGIC", # MAGIC_POWDER, MAGIC_ROD
"MESSAGE", # MESSAGE (Master Stalfos' Message)
"PEGASUS", # PEGASUS_BOOTS
"PIECE", # HEART_PIECE, PIECE_OF_POWER
"POWER", # POWER_BRACELET, PIECE_OF_POWER
"SINGLE", # SINGLE_ARROW
"STONE", # STONE_BEAK
"BEAK1",
"BEAK2",
"BEAK3",
"BEAK4",
"BEAK5",
"BEAK6",
"BEAK7",
"BEAK8",
"COMPASS1",
"COMPASS2",
"COMPASS3",
"COMPASS4",
"COMPASS5",
"COMPASS6",
"COMPASS7",
"COMPASS8",
"MAP1",
"MAP2",
"MAP3",
"MAP4",
"MAP5",
"MAP6",
"MAP7",
"MAP8",
]
# Single word synonyms for Link's Awakening items, for generic matching.
SYNONYMS = {
# POWER_BRACELET
'ANKLET': 'POWER_BRACELET',
'ARMLET': 'POWER_BRACELET',
'BAND': 'POWER_BRACELET',
'BANGLE': 'POWER_BRACELET',
'BRACER': 'POWER_BRACELET',
'CARRY': 'POWER_BRACELET',
'CIRCLET': 'POWER_BRACELET',
'CROISSANT': 'POWER_BRACELET',
'GAUNTLET': 'POWER_BRACELET',
'GLOVE': 'POWER_BRACELET',
'RING': 'POWER_BRACELET',
'STRENGTH': 'POWER_BRACELET',
# SHIELD
'AEGIS': 'SHIELD',
'BUCKLER': 'SHIELD',
'SHLD': 'SHIELD',
# BOW
'BALLISTA': 'BOW',
# HOOKSHOT
'GRAPPLE': 'HOOKSHOT',
'GRAPPLING': 'HOOKSHOT',
'ROPE': 'HOOKSHOT',
# MAGIC_ROD
'BEAM': 'MAGIC_ROD',
'CANE': 'MAGIC_ROD',
'STAFF': 'MAGIC_ROD',
'WAND': 'MAGIC_ROD',
# PEGASUS_BOOTS
'BOOT': 'PEGASUS_BOOTS',
'GREAVES': 'PEGASUS_BOOTS',
'RUN': 'PEGASUS_BOOTS',
'SHOE': 'PEGASUS_BOOTS',
'SPEED': 'PEGASUS_BOOTS',
# OCARINA
'FLUTE': 'OCARINA',
'RECORDER': 'OCARINA',
# FEATHER
'JUMP': 'FEATHER',
'PLUME': 'FEATHER',
'WING': 'FEATHER',
# SHOVEL
'DIG': 'SHOVEL',
# MAGIC_POWDER
'BAG': 'MAGIC_POWDER',
'CASE': 'MAGIC_POWDER',
'DUST': 'MAGIC_POWDER',
'POUCH': 'MAGIC_POWDER',
'SACK': 'MAGIC_POWDER',
# BOMB
'BLAST': 'BOMB',
'BOMBCHU': 'BOMB',
'FIRECRACKER': 'BOMB',
'TNT': 'BOMB',
# SWORD
'BLADE': 'SWORD',
'CUT': 'SWORD',
'DAGGER': 'SWORD',
'DIRK': 'SWORD',
'EDGE': 'SWORD',
'EPEE': 'SWORD',
'EXCALIBUR': 'SWORD',
'FALCHION': 'SWORD',
'KATANA': 'SWORD',
'KNIFE': 'SWORD',
'MACHETE': 'SWORD',
'MASAMUNE': 'SWORD',
'MURASAME': 'SWORD',
'SABER': 'SWORD',
'SABRE': 'SWORD',
'SCIMITAR': 'SWORD',
'SLASH': 'SWORD',
# FLIPPERS
'FLIPPER': 'FLIPPERS',
'SWIM': 'FLIPPERS',
# MEDICINE
'BOTTLE': 'MEDICINE',
'FLASK': 'MEDICINE',
'LEMONADE': 'MEDICINE',
'POTION': 'MEDICINE',
'TEA': 'MEDICINE',
# TAIL_KEY
# ANGLER_KEY
# FACE_KEY
# BIRD_KEY
# SLIME_KEY
# GOLD_LEAF
'HERB': 'GOLD_LEAF',
# RUPEES_20
'COIN': 'RUPEES_20',
'MONEY': 'RUPEES_20',
'RUPEE': 'RUPEES_20',
# RUPEES_50
# RUPEES_100
# RUPEES_200
# RUPEES_500
'GEM': 'RUPEES_500',
'JEWEL': 'RUPEES_500',
# SEASHELL
'CARAPACE': 'SEASHELL',
'CONCH': 'SEASHELL',
'SHELL': 'SEASHELL',
# MESSAGE (master stalfos message)
'NOTHING': 'MESSAGE',
'TRAP': 'MESSAGE',
# BOOMERANG
'BOOMER': 'BOOMERANG',
# HEART_PIECE
# BOWWOW
'BEAST': 'BOWWOW',
'PET': 'BOWWOW',
# ARROWS_10
# SINGLE_ARROW
'MISSILE': 'SINGLE_ARROW',
'QUIVER': 'SINGLE_ARROW',
# ROOSTER
'BIRD': 'ROOSTER',
'CHICKEN': 'ROOSTER',
'CUCCO': 'ROOSTER',
'FLY': 'ROOSTER',
'GRIFFIN': 'ROOSTER',
'GRYPHON': 'ROOSTER',
# MAX_POWDER_UPGRADE
# MAX_BOMBS_UPGRADE
# MAX_ARROWS_UPGRADE
# RED_TUNIC
# BLUE_TUNIC
'ARMOR': 'BLUE_TUNIC',
'MAIL': 'BLUE_TUNIC',
'SUIT': 'BLUE_TUNIC',
# HEART_CONTAINER
'TANK': 'HEART_CONTAINER',
# TOADSTOOL
'FUNGAL': 'TOADSTOOL',
'FUNGUS': 'TOADSTOOL',
'MUSHROOM': 'TOADSTOOL',
'SHROOM': 'TOADSTOOL',
# GUARDIAN_ACORN
'NUT': 'GUARDIAN_ACORN',
'SEED': 'GUARDIAN_ACORN',
# KEY
'DOOR': 'KEY',
'GATE': 'KEY',
'KEY': 'KEY', # Without this, foreign keys show up as nightmare keys
'LOCK': 'KEY',
'PANEL': 'KEY',
'UNLOCK': 'KEY',
# NIGHTMARE_KEY
# MAP
# COMPASS
# STONE_BEAK
'FOSSIL': 'STONE_BEAK',
'RELIC': 'STONE_BEAK',
# SONG1
'BOLERO': 'SONG1',
'LULLABY': 'SONG1',
'MELODY': 'SONG1',
'MINUET': 'SONG1',
'NOCTURNE': 'SONG1',
'PRELUDE': 'SONG1',
'REQUIEM': 'SONG1',
'SERENADE': 'SONG1',
'SONG': 'SONG1',
# SONG2
'FISH': 'SONG2',
'SURF': 'SONG2',
# SONG3
'FROG': 'SONG3',
# INSTRUMENT1
'CELLO': 'INSTRUMENT1',
'GUITAR': 'INSTRUMENT1',
'LUTE': 'INSTRUMENT1',
'VIOLIN': 'INSTRUMENT1',
# INSTRUMENT2
'HORN': 'INSTRUMENT2',
# INSTRUMENT3
'BELL': 'INSTRUMENT3',
'CHIME': 'INSTRUMENT3',
# INSTRUMENT4
'HARP': 'INSTRUMENT4',
'KANTELE': 'INSTRUMENT4',
# INSTRUMENT5
'MARIMBA': 'INSTRUMENT5',
'XYLOPHONE': 'INSTRUMENT5',
# INSTRUMENT6 (triangle)
# INSTRUMENT7
'KEYBOARD': 'INSTRUMENT7',
'ORGAN': 'INSTRUMENT7',
'PIANO': 'INSTRUMENT7',
# INSTRUMENT8
'DRUM': 'INSTRUMENT8',
# TRADING_ITEM_YOSHI_DOLL
'DINOSAUR': 'TRADING_ITEM_YOSHI_DOLL',
'DRAGON': 'TRADING_ITEM_YOSHI_DOLL',
'TOY': 'TRADING_ITEM_YOSHI_DOLL',
# TRADING_ITEM_RIBBON
'HAIRBAND': 'TRADING_ITEM_RIBBON',
'HAIRPIN': 'TRADING_ITEM_RIBBON',
# TRADING_ITEM_DOG_FOOD
'CAN': 'TRADING_ITEM_DOG_FOOD',
# TRADING_ITEM_BANANAS
'BANANA': 'TRADING_ITEM_BANANAS',
# TRADING_ITEM_STICK
'BRANCH': 'TRADING_ITEM_STICK',
'TWIG': 'TRADING_ITEM_STICK',
# TRADING_ITEM_HONEYCOMB
'BEEHIVE': 'TRADING_ITEM_HONEYCOMB',
'HIVE': 'TRADING_ITEM_HONEYCOMB',
'HONEY': 'TRADING_ITEM_HONEYCOMB',
# TRADING_ITEM_PINEAPPLE
'FOOD': 'TRADING_ITEM_PINEAPPLE',
'FRUIT': 'TRADING_ITEM_PINEAPPLE',
'GOURD': 'TRADING_ITEM_PINEAPPLE',
# TRADING_ITEM_HIBISCUS
'FLOWER': 'TRADING_ITEM_HIBISCUS',
'PETAL': 'TRADING_ITEM_HIBISCUS',
# TRADING_ITEM_LETTER
'CARD': 'TRADING_ITEM_LETTER',
'MESSAGE': 'TRADING_ITEM_LETTER',
# TRADING_ITEM_BROOM
'SWEEP': 'TRADING_ITEM_BROOM',
# TRADING_ITEM_FISHING_HOOK
'CLAW': 'TRADING_ITEM_FISHING_HOOK',
# TRADING_ITEM_NECKLACE
'AMULET': 'TRADING_ITEM_NECKLACE',
'BEADS': 'TRADING_ITEM_NECKLACE',
'PEARLS': 'TRADING_ITEM_NECKLACE',
'PENDANT': 'TRADING_ITEM_NECKLACE',
'ROSARY': 'TRADING_ITEM_NECKLACE',
# TRADING_ITEM_SCALE
# TRADING_ITEM_MAGNIFYING_GLASS
'FINDER': 'TRADING_ITEM_MAGNIFYING_GLASS',
'LENS': 'TRADING_ITEM_MAGNIFYING_GLASS',
'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
'SCOPE': 'TRADING_ITEM_MAGNIFYING_GLASS',
'XRAY': 'TRADING_ITEM_MAGNIFYING_GLASS',
# PIECE_OF_POWER
'TRIANGLE': 'PIECE_OF_POWER',
'POWER': 'PIECE_OF_POWER',
'TRIFORCE': 'PIECE_OF_POWER',
}
# For generic multi-word matches.
PHRASES = {
'BIG KEY': 'NIGHTMARE_KEY',
'BOSS KEY': 'NIGHTMARE_KEY',
'HEART PIECE': 'HEART_PIECE',
'PIECE OF HEART': 'HEART_PIECE',
}
# All following will only be used to match items for the specific game.
# Item names will be uppercased when comparing.
# Can be multi-word.
GAME_SPECIFIC_PHRASES = {
'Final Fantasy': {
'OXYALE': 'MEDICINE',
'VORPAL': 'SWORD',
'XCALBER': 'SWORD',
},
'The Legend of Zelda': {
'WATER OF LIFE': 'MEDICINE',
},
'The Legend of Zelda - Oracle of Seasons': {
'RARE PEACH STONE': 'HEART_PIECE',
},
'Noita': {
'ALL-SEEING EYE': 'TRADING_ITEM_MAGNIFYING_GLASS', # lets you find secrets
},
'Ocarina of Time': {
'COJIRO': 'ROOSTER',
},
'SMZ3': {
'BIGKEY': 'NIGHTMARE_KEY',
'BYRNA': 'MAGIC_ROD',
'HEARTPIECE': 'HEART_PIECE',
'POWERBOMB': 'BOMB',
'SOMARIA': 'MAGIC_ROD',
'SUPER': 'SINGLE_ARROW',
},
'Sonic Adventure 2 Battle': {
'CHAOS EMERALD': 'PIECE_OF_POWER',
},
'Super Mario 64': {
'POWER STAR': 'PIECE_OF_POWER',
},
'Super Mario World': {
'P-BALLOON': 'FEATHER',
},
'Super Metroid': {
'POWER BOMB': 'BOMB',
},
'The Witness': {
'BONK': 'BOMB',
'BUNKER LASER': 'INSTRUMENT4',
'DESERT LASER': 'INSTRUMENT5',
'JUNGLE LASER': 'INSTRUMENT4',
'KEEP LASER': 'INSTRUMENT7',
'MONASTERY LASER': 'INSTRUMENT1',
'POWER SURGE': 'BOMB',
'PUZZLE SKIP': 'GOLD_LEAF',
'QUARRY LASER': 'INSTRUMENT8',
'SHADOWS LASER': 'INSTRUMENT1',
'SHORTCUTS': 'KEY',
'SLOWNESS': 'BOMB',
'SWAMP LASER': 'INSTRUMENT2',
'SYMMETRY LASER': 'INSTRUMENT6',
'TOWN LASER': 'INSTRUMENT3',
'TREEHOUSE LASER': 'INSTRUMENT2',
'WATER PUMPS': 'KEY',
},
'TUNIC': {
"AURA'S GEM": 'SHIELD', # card that enhances the shield
'DUSTY': 'TRADING_ITEM_BROOM', # a broom
'HERO RELIC - HP': 'TRADING_ITEM_HIBISCUS',
'HERO RELIC - MP': 'TOADSTOOL',
'HERO RELIC - SP': 'FEATHER',
'HP BERRY': 'GUARDIAN_ACORN',
'HP OFFERING': 'TRADING_ITEM_HIBISCUS', # a flower
'LUCKY CUP': 'HEART_CONTAINER', # card with a heart on it
'INVERTED ASH': 'MEDICINE', # card with a potion on it
'MAGIC ORB': 'HOOKSHOT',
'MP BERRY': 'GUARDIAN_ACORN',
'MP OFFERING': 'TOADSTOOL', # a mushroom
'QUESTAGON': 'PIECE_OF_POWER', # triforce piece equivalent
'SP OFFERING': 'FEATHER', # a feather
'SPRING FALLS': 'TRADING_ITEM_HIBISCUS', # a flower
},
'FNaFW': {
'Freddy': 'TRADING_ITEM_YOSHI_DOLL', # all of these are animatronics, aka dolls.
'Bonnie': 'TRADING_ITEM_YOSHI_DOLL',
'Chica': 'TRADING_ITEM_YOSHI_DOLL',
'Foxy': 'TRADING_ITEM_YOSHI_DOLL',
'Toy Bonnie': 'TRADING_ITEM_YOSHI_DOLL',
'Toy Chica': 'TRADING_ITEM_YOSHI_DOLL',
'Toy Freddy': 'TRADING_ITEM_YOSHI_DOLL',
'Mangle': 'TRADING_ITEM_YOSHI_DOLL',
'Balloon Boy': 'TRADING_ITEM_YOSHI_DOLL',
'JJ': 'TRADING_ITEM_YOSHI_DOLL',
'Phantom Freddy': 'TRADING_ITEM_YOSHI_DOLL',
'Phantom BB': 'TRADING_ITEM_YOSHI_DOLL',
'Phantom Chica': 'TRADING_ITEM_YOSHI_DOLL',
'Phantom Mangle': 'TRADING_ITEM_YOSHI_DOLL',
'Withered Foxy': 'TRADING_ITEM_YOSHI_DOLL',
'Phantom Foxy': 'TRADING_ITEM_YOSHI_DOLL',
'Withered Chica': 'TRADING_ITEM_YOSHI_DOLL',
'Withered Freddy': 'TRADING_ITEM_YOSHI_DOLL',
'Withered Bonnie': 'TRADING_ITEM_YOSHI_DOLL',
'Shadow Freddy': 'TRADING_ITEM_YOSHI_DOLL',
'Marionette': 'TRADING_ITEM_YOSHI_DOLL',
'Phantom Marionette': 'TRADING_ITEM_YOSHI_DOLL',
'Golden Freddy': 'TRADING_ITEM_YOSHI_DOLL',
'Paperpals': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare Freddy': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare Bonnie': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare Chica': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare Foxy': 'TRADING_ITEM_YOSHI_DOLL',
'Endo 01': 'TRADING_ITEM_YOSHI_DOLL',
'Endo 02': 'TRADING_ITEM_YOSHI_DOLL',
'Plushtrap': 'TRADING_ITEM_YOSHI_DOLL',
'Endoplush': 'TRADING_ITEM_YOSHI_DOLL',
'Springtrap': 'TRADING_ITEM_YOSHI_DOLL',
'RWQFSFASXC': 'TRADING_ITEM_YOSHI_DOLL',
'Crying Child': 'TRADING_ITEM_YOSHI_DOLL',
'Funtime Foxy': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare Fredbear': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare': 'TRADING_ITEM_YOSHI_DOLL',
'Fredbear': 'TRADING_ITEM_YOSHI_DOLL',
'Spring Bonnie': 'TRADING_ITEM_YOSHI_DOLL',
'Jack-O-Chica': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmare BB': 'TRADING_ITEM_YOSHI_DOLL',
'Coffee': 'TRADING_ITEM_YOSHI_DOLL',
'Jack-O-Bonnie': 'TRADING_ITEM_YOSHI_DOLL',
'Purpleguy': 'TRADING_ITEM_YOSHI_DOLL',
'Nightmarionne': 'TRADING_ITEM_YOSHI_DOLL',
'Mr. Chipper': 'TRADING_ITEM_YOSHI_DOLL',
'Animdude': 'TRADING_ITEM_YOSHI_DOLL',
'Progressive Endoskeleton': 'BLUE_TUNIC', # basically armor you wear to give you more defense
'25 Tokens': 'RUPEES_20', # money
'50 Tokens': 'RUPEES_50',
'100 Tokens': 'RUPEES_100',
'250 Tokens': 'RUPEES_200',
'500 Tokens': 'RUPEES_500',
'1000 Tokens': 'RUPEES_500',
'2500 Tokens': 'RUPEES_500',
'5000 Tokens': 'RUPEES_500',
},
}

View File

@@ -98,6 +98,7 @@ class ItemName:
HEART_CONTAINER = "Heart Container"
BAD_HEART_CONTAINER = "Bad Heart Container"
TOADSTOOL = "Toadstool"
GUARDIAN_ACORN = "Guardian Acorn"
KEY = "Key"
KEY1 = "Small Key (Tail Cave)"
KEY2 = "Small Key (Bottle Grotto)"
@@ -173,6 +174,7 @@ class ItemName:
TRADING_ITEM_NECKLACE = "Necklace"
TRADING_ITEM_SCALE = "Scale"
TRADING_ITEM_MAGNIFYING_GLASS = "Magnifying Glass"
PIECE_OF_POWER = "Piece Of Power"
trade_item_prog = ItemClassification.progression
@@ -219,6 +221,7 @@ links_awakening_items = [
ItemData(ItemName.HEART_CONTAINER, "HEART_CONTAINER", ItemClassification.useful),
#ItemData(ItemName.BAD_HEART_CONTAINER, "BAD_HEART_CONTAINER", ItemClassification.trap),
ItemData(ItemName.TOADSTOOL, "TOADSTOOL", ItemClassification.progression),
ItemData(ItemName.GUARDIAN_ACORN, "GUARDIAN_ACORN", ItemClassification.filler),
DungeonItemData(ItemName.KEY, "KEY", ItemClassification.progression),
DungeonItemData(ItemName.KEY1, "KEY1", ItemClassification.progression),
DungeonItemData(ItemName.KEY2, "KEY2", ItemClassification.progression),
@@ -293,7 +296,8 @@ links_awakening_items = [
TradeItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog, "Grandma (Animal Village)"),
TradeItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog, "Fisher (Martha's Bay)"),
TradeItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog, "Mermaid (Martha's Bay)"),
TradeItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog, "Mermaid Statue (Martha's Bay)")
TradeItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog, "Mermaid Statue (Martha's Bay)"),
ItemData(ItemName.PIECE_OF_POWER, "PIECE_OF_POWER", ItemClassification.filler),
]
ladxr_item_to_la_item_name = {

View File

@@ -58,7 +58,6 @@ from . import hints
from .patches import bank34
from .utils import formatText
from ..Options import TrendyGame, Palette, Warps
from .roomEditor import RoomEditor, Object
from .patches.aesthetics import rgb_to_bin, bin_to_rgb
@@ -66,7 +65,7 @@ from .locations.keyLocation import KeyLocation
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
from ..Options import TrendyGame, Palette, MusicChangeCondition, Warps
if TYPE_CHECKING:
from .. import LinksAwakeningWorld
@@ -156,6 +155,8 @@ def generateRom(args, world: "LinksAwakeningWorld"):
if not world.ladxr_settings.rooster:
patches.maptweaks.tweakMap(rom)
patches.maptweaks.tweakBirdKeyRoom(rom)
if world.ladxr_settings.overworld == "openmabe":
patches.maptweaks.openMabe(rom)
patches.chest.fixChests(rom)
patches.shop.fixShop(rom)
patches.rooster.patchRooster(rom)
@@ -247,7 +248,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.core.quickswap(rom, 1)
elif world.ladxr_settings.quickswap == 'b':
patches.core.quickswap(rom, 0)
patches.core.addBootsControls(rom, world.options.boots_controls)
@@ -397,7 +398,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
# Then put new text in
for bucket_idx, (orig_idx, data) in enumerate(bucket):
rom.texts[shuffled[bucket_idx][0]] = data
if world.options.trendy_game != TrendyGame.option_normal:

View File

@@ -87,6 +87,8 @@ CHEST_ITEMS = {
TOADSTOOL: 0x50,
GUARDIAN_ACORN: 0x51,
HEART_PIECE: 0x80,
BOWWOW: 0x81,
ARROWS_10: 0x82,
@@ -128,4 +130,6 @@ CHEST_ITEMS = {
TRADING_ITEM_NECKLACE: 0xA2,
TRADING_ITEM_SCALE: 0xA3,
TRADING_ITEM_MAGNIFYING_GLASS: 0xA4,
PIECE_OF_POWER: 0xA5,
}

View File

@@ -44,6 +44,8 @@ BAD_HEART_CONTAINER = "BAD_HEART_CONTAINER"
TOADSTOOL = "TOADSTOOL"
GUARDIAN_ACORN = "GUARDIAN_ACORN"
KEY = "KEY"
KEY1 = "KEY1"
KEY2 = "KEY2"
@@ -124,3 +126,5 @@ TRADING_ITEM_FISHING_HOOK = "TRADING_ITEM_FISHING_HOOK"
TRADING_ITEM_NECKLACE = "TRADING_ITEM_NECKLACE"
TRADING_ITEM_SCALE = "TRADING_ITEM_SCALE"
TRADING_ITEM_MAGNIFYING_GLASS = "TRADING_ITEM_MAGNIFYING_GLASS"
PIECE_OF_POWER = "PIECE_OF_POWER"

View File

@@ -144,7 +144,12 @@ class World:
self._addEntrance("moblin_cave", graveyard, moblin_cave, None)
# "Ukuku Prairie"
ukuku_prairie = Location().connect(mabe_village, POWER_BRACELET).connect(graveyard, POWER_BRACELET)
ukuku_prairie = Location()
if options.overworld == "openmabe":
ukuku_prairie.connect(mabe_village, r.bush)
else:
ukuku_prairie.connect(mabe_village, POWER_BRACELET)
ukuku_prairie.connect(graveyard, POWER_BRACELET)
ukuku_prairie.connect(Location().add(TradeSequenceItem(0x07B, TRADING_ITEM_STICK)), TRADING_ITEM_BANANAS)
ukuku_prairie.connect(Location().add(TradeSequenceItem(0x087, TRADING_ITEM_HONEYCOMB)), TRADING_ITEM_STICK)
self._addEntrance("prairie_left_phone", ukuku_prairie, None, None)

View File

@@ -835,6 +835,7 @@ ItemSpriteTable:
db $46, $1C ; NIGHTMARE_KEY8
db $46, $1C ; NIGHTMARE_KEY9
db $4C, $1C ; Toadstool
db $AE, $14 ; Guardian Acorn
LargeItemSpriteTable:
db $AC, $02, $AC, $22 ; heart piece
@@ -874,6 +875,7 @@ LargeItemSpriteTable:
db $D8, $0D, $DA, $0D ; TradeItem12
db $DC, $0D, $DE, $0D ; TradeItem13
db $E0, $0D, $E2, $0D ; TradeItem14
db $14, $42, $14, $62 ; Piece Of Power
ItemMessageTable:
db $90, $3D, $89, $93, $94, $95, $96, $97, $98, $99, $9A, $9B, $9C, $9D, $D9, $A2
@@ -888,7 +890,7 @@ ItemMessageTable:
; $80
db $4F, $C8, $CA, $CB, $E2, $E3, $E4, $CC, $CD, $2A, $2B, $C9, $C9, $C9, $C9, $C9
db $C9, $C9, $C9, $C9, $C9, $C9, $B8, $44, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9
db $C9, $C9, $C9, $C9, $9D
db $C9, $C9, $C9, $C9, $9D, $C9
RenderDroppedKey:
;TODO: See EntityInitKeyDropPoint for a few special cases to unload.

View File

@@ -170,7 +170,7 @@ ItemNamePointers:
dw ItemNameNightmareKey8
dw ItemNameNightmareKey9
dw ItemNameToadstool
dw ItemNameNone ; 0x51
dw ItemNameGuardianAcorn
dw ItemNameNone ; 0x52
dw ItemNameNone ; 0x53
dw ItemNameNone ; 0x54
@@ -254,6 +254,7 @@ ItemNamePointers:
dw ItemTradeQuest12
dw ItemTradeQuest13
dw ItemTradeQuest14
dw ItemPieceOfPower
ItemNameNone:
db m"NONE", $ff
@@ -418,6 +419,8 @@ ItemNameNightmareKey9:
db m"Got the {NIGHTMARE_KEY9}", $ff
ItemNameToadstool:
db m"Got the {TOADSTOOL}", $ff
ItemNameGuardianAcorn:
db m"Got a Guardian Acorn", $ff
ItemNameHeartPiece:
db m"Got the {HEART_PIECE}", $ff
@@ -496,5 +499,8 @@ ItemTradeQuest13:
db m"You've got the Scale", $ff
ItemTradeQuest14:
db m"You've got the Magnifying Lens", $ff
ItemPieceOfPower:
db m"You've got a Piece of Power", $ff
MultiNamePointers:

View File

@@ -96,7 +96,9 @@ StartGameMarinMessage:
ldi [hl], a ;hour counter
ld hl, $B010
ld a, $01 ;tarin's gift gets skipped for some reason, so inflate count by 1
ldi [hl], a ;check counter low
xor a
ldi [hl], a ;check counter high
; Show the normal message

View File

@@ -24,14 +24,10 @@ notSpecialSideView:
ld a, $06 ; giveItemMultiworld
rst 8
ldh a, [$F1] ; Load active sprite variant to see if this is just a normal small key
cp $1A
jr z, isAKey
;Show message (if not a key)
;Show message
ld a, $0A ; showMessageMultiworld
rst 8
isAKey:
ret
"""))
rom.patch(0x03, 0x24B7, "3E", "3E") # sanity check

View File

@@ -38,3 +38,12 @@ def tweakBirdKeyRoom(rom):
re.moveObject(2, 5, 3, 6)
re.addEntity(3, 5, 0x9D)
re.store(rom)
def openMabe(rom):
# replaces rocks on east side of Mabe Village with bushes
re = RoomEditor(rom, 0x094)
re.changeObject(5, 1, 0x5C)
re.overlay[5 + 1 * 10] = 0x5C
re.overlay[5 + 2 * 10] = 0x5C
re.store(rom)

View File

@@ -169,7 +169,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
[Never] you can never steal from the shop."""),
Setting('bowwow', 'Special', 'g', 'Good boy mode', options=[('normal', '', 'Disabled'), ('always', 'a', 'Enabled'), ('swordless', 's', 'Enabled (swordless)')], default='normal',
description='Allows BowWow to be taken into any area, damage bosses and more enemies. If enabled you always start with bowwow. Swordless option removes the swords from the game and requires you to beat the game without a sword and just bowwow.'),
Setting('overworld', 'Special', 'O', 'Overworld', options=[('normal', '', 'Normal'), ('dungeondive', 'D', 'Dungeon dive'), ('nodungeons', 'N', 'No dungeons'), ('random', 'R', 'Randomized')], default='normal',
Setting('overworld', 'Special', 'O', 'Overworld', options=[('normal', '', 'Normal'), ('dungeondive', 'D', 'Dungeon dive'), ('nodungeons', 'N', 'No dungeons'), ('random', 'R', 'Randomized'), ('openmabe', 'M', 'Open Mabe')], default='normal',
description="""
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
[No dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed.
@@ -181,7 +181,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
aesthetic=True),
Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('normal', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
description="""[Fast] makes text appear twice as fast.
[No-Text] removes all text from the game""", aesthetic=True),
Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow',

View File

@@ -57,7 +57,7 @@ class TextShuffle(DefaultOffToggle):
class Rooster(DefaultOnToggle, LADXROption):
"""
[On] Adds the rooster to the item pool.
[On] Adds the rooster to the item pool.
[Off] The rooster spot is still a check giving an item. But you will never find the rooster. In that case, any rooster spot is accessible without rooster by other means.
"""
display_name = "Rooster"
@@ -70,7 +70,7 @@ class Boomerang(Choice):
[Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled.
"""
display_name = "Boomerang"
normal = 0
gift = 1
default = gift
@@ -156,7 +156,7 @@ class ShuffleSmallKeys(DungeonItemShuffle):
[Own Dungeons] The item will be within a dungeon in your world
[Own World] The item will be somewhere in your world
[Any World] The item could be anywhere
[Different World] The item will be somewhere in another world
[Different World] The item will be somewhere in another world
"""
display_name = "Shuffle Small Keys"
ladxr_item = "KEY"
@@ -223,7 +223,7 @@ class Goal(Choice, LADXROption):
The Goal of the game
[Instruments] The Wind Fish's Egg will only open if you have the required number of Instruments of the Sirens, and play the Ballad of the Wind Fish.
[Seashells] The Egg will open when you bring 20 seashells. The Ballad and Ocarina are not needed.
[Open] The Egg will start pre-opened.
[Open] The Egg will start pre-opened.
"""
display_name = "Goal"
ladxr_name = "goal"
@@ -278,11 +278,21 @@ class MusicChangeCondition(Choice):
# [Start with 1] normal game, you just start with 1 heart instead of 3.
# [Low max] replace heart containers with heart pieces."""),
# Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none',
# description="""
# [Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn.
# [Hero] Switch version hero mode, double damage, no heart/fairy drops.
# [One hit KO] You die on a single hit, always."""),
class HardMode(Choice, LADXROption):
"""
[Oracle] Less iframes and health from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn.
[Hero] Switch version hero mode, double damage, no heart/fairy drops.
[One hit KO] You die on a single hit, always.
"""
display_name = "Hard Mode"
ladxr_name = "hardmode"
option_none = 0
option_oracle = 1
option_hero = 2
option_ohko = 3
default = option_none
# Setting('steal', 'Gameplay', 't', 'Stealing from the shop',
# options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default',
@@ -303,49 +313,61 @@ class Bowwow(Choice):
class Overworld(Choice, LADXROption):
"""
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
[Tiny dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed.
[Open Mabe] Replaces rock on the east side of Mabe Village with bushes, allowing access to Ukuku Prairie without Power Bracelet.
"""
display_name = "Overworld"
ladxr_name = "overworld"
option_normal = 0
option_dungeon_dive = 1
option_tiny_dungeons = 2
# option_shuffled = 3
option_open_mabe = 1
default = option_normal
# Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
# Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
# aesthetic=True),
# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
# description="""[Fast] makes text appear twice as fast.
# [No-Text] removes all text from the game""", aesthetic=True),
# Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow',
# description='Slows or disables the low health beeping sound', aesthetic=True),
# Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True,
# description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.',
# aesthetic=True),
# Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False,
# description='Enables the nag messages normally shown when touching stones and crystals',
# aesthetic=True),
# Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='',
# description='Generally affects at least Link\'s sprite, but can alter any graphics in the game',
# aesthetic=True),
# Setting('linkspalette', 'User options', 'C', "Link's color",
# options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'),
# ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True,
# description="""Allows you to force a certain color on link.
# [Normal] color of link depends on the tunic.
# [Green/Yellow/Red/Blue] forces link into one of these colors.
# [?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""),
# Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='',
# description="""
# [Random] Randomizes overworld and dungeon music'
# [Disable] no music in the whole game""",
# aesthetic=True),
class Quickswap(Choice, LADXROption):
"""
Adds that the SELECT button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.
"""
display_name = "Quickswap"
ladxr_name = "quickswap"
option_none = 0
option_a = 1
option_b = 2
default = option_none
class TextMode(Choice, LADXROption):
"""
[Fast] Makes text appear twice as fast
"""
display_name = "Text Mode"
ladxr_name = "textmode"
option_normal = 0
option_fast = 1
default = option_fast
class LowHpBeep(Choice, LADXROption):
"""
Slows or disables the low health beeping sound.
"""
display_name = "Low HP Beep"
ladxr_name = "lowhpbeep"
option_default = 0
option_slow = 1
option_none = 2
default = option_default
class NoFlash(DefaultOnToggle, LADXROption):
"""
Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive to these things.
"""
display_name = "No Flash"
ladxr_name = "noflash"
class BootsControls(Choice):
"""
@@ -447,7 +469,7 @@ class GfxMod(FreeText, LADXROption):
class Palette(Choice):
"""
Sets the palette for the game.
Sets the palette for the game.
Note: A few places aren't patched, such as the menu and a few color dungeon tiles.
[Normal] The vanilla palette
[1-Bit] One bit of color per channel
@@ -505,6 +527,18 @@ class InGameHints(DefaultOnToggle):
display_name = "In-game Hints"
class ForeignItemIcons(Choice):
"""
Choose how to display foreign items.
[Guess By Name] Foreign items can look like any Link's Awakening item.
[Indicate Progression] Foreign items are either a Piece of Power (progression) or Guardian Acorn (non-progression).
"""
display_name = "Foreign Item Icons"
option_guess_by_name = 0
option_indicate_progression = 1
default = option_guess_by_name
ladx_option_groups = [
OptionGroup("Goal Options", [
Goal,
@@ -524,9 +558,12 @@ ladx_option_groups = [
OptionGroup("Miscellaneous", [
TradeQuest,
Rooster,
Overworld,
TrendyGame,
InGameHints,
NagMessages,
Quickswap,
HardMode,
BootsControls
]),
OptionGroup("Experimental", [
@@ -537,22 +574,26 @@ ladx_option_groups = [
LinkPalette,
Palette,
TextShuffle,
ForeignItemIcons,
APTitleScreen,
GfxMod,
Music,
MusicChangeCondition
MusicChangeCondition,
LowHpBeep,
TextMode,
NoFlash,
])
]
@dataclass
class LinksAwakeningOptions(PerGameCommonOptions):
logic: Logic
# 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'),
# 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'),
# 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'),
# 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
# 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'),
# 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'),
# 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'),
# 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
tradequest: TradeQuest # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
# 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
# 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
rooster: Rooster # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
# 'boomerang': Boomerang,
# 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'),
@@ -571,6 +612,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
gfxmod: GfxMod
palette: Palette
text_shuffle: TextShuffle
foreign_item_icons: ForeignItemIcons
shuffle_nightmare_keys: ShuffleNightmareKeys
shuffle_small_keys: ShuffleSmallKeys
shuffle_maps: ShuffleMaps
@@ -582,7 +624,13 @@ class LinksAwakeningOptions(PerGameCommonOptions):
nag_messages: NagMessages
ap_title_screen: APTitleScreen
boots_controls: BootsControls
quickswap: Quickswap
hard_mode: HardMode
low_hp_beep: LowHpBeep
text_mode: TextMode
no_flash: NoFlash
in_game_hints: InGameHints
overworld: Overworld
warp_improvements: Removed
additional_warp_points: Removed

View File

@@ -4,6 +4,7 @@ import os
import pkgutil
import tempfile
import typing
import re
import bsdiff4
@@ -12,6 +13,7 @@ from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial,
from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World
from .Common import *
from . import ItemIconGuessing
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name,
links_awakening_item_name_groups)
@@ -380,66 +382,36 @@ class LinksAwakeningWorld(World):
name_cache = {}
# Tries to associate an icon from another game with an icon we have
def guess_icon_for_other_world(self, other):
def guess_icon_for_other_world(self, foreign_item):
if not self.name_cache:
forbidden = [
"TRADING",
"ITEM",
"BAD",
"SINGLE",
"UPGRADE",
"BLUE",
"RED",
"NOTHING",
"MESSAGE",
]
for item in ladxr_item_to_la_item_name.keys():
self.name_cache[item] = item
splits = item.split("_")
self.name_cache["".join(splits)] = item
if 'RUPEES' in splits:
self.name_cache["".join(reversed(splits))] = item
for word in item.split("_"):
if word not in forbidden and not word.isnumeric():
if word not in ItemIconGuessing.BLOCKED_ASSOCIATIONS and not word.isnumeric():
self.name_cache[word] = item
others = {
'KEY': 'KEY',
'COMPASS': 'COMPASS',
'BIGKEY': 'NIGHTMARE_KEY',
'MAP': 'MAP',
'FLUTE': 'OCARINA',
'SONG': 'OCARINA',
'MUSHROOM': 'TOADSTOOL',
'GLOVE': 'POWER_BRACELET',
'BOOT': 'PEGASUS_BOOTS',
'SHOE': 'PEGASUS_BOOTS',
'SHOES': 'PEGASUS_BOOTS',
'SANCTUARYHEARTCONTAINER': 'HEART_CONTAINER',
'BOSSHEARTCONTAINER': 'HEART_CONTAINER',
'HEARTCONTAINER': 'HEART_CONTAINER',
'ENERGYTANK': 'HEART_CONTAINER',
'MISSILE': 'SINGLE_ARROW',
'BOMBS': 'BOMB',
'BLUEBOOMERANG': 'BOOMERANG',
'MAGICMIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
'MESSAGE': 'TRADING_ITEM_LETTER',
# TODO: Also use AP item name
}
for name in others.values():
for name in ItemIconGuessing.SYNONYMS.values():
assert name in self.name_cache, name
assert name in CHEST_ITEMS, name
self.name_cache.update(others)
uppered = other.upper()
if "BIG KEY" in uppered:
return 'NIGHTMARE_KEY'
possibles = other.upper().split(" ")
rejoined = "".join(possibles)
if rejoined in self.name_cache:
return self.name_cache[rejoined]
self.name_cache.update(ItemIconGuessing.SYNONYMS)
pluralizations = {k + "S": v for k, v in self.name_cache.items()}
self.name_cache = pluralizations | self.name_cache
uppered = foreign_item.name.upper()
foreign_game = self.multiworld.game[foreign_item.player]
phrases = ItemIconGuessing.PHRASES.copy()
if foreign_game in ItemIconGuessing.GAME_SPECIFIC_PHRASES:
phrases.update(ItemIconGuessing.GAME_SPECIFIC_PHRASES[foreign_game])
for phrase, icon in phrases.items():
if phrase in uppered:
return icon
# pattern for breaking down camelCase, also separates out digits
pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=\d)")
possibles = pattern.sub(' ', foreign_item.name).upper()
for ch in "[]()_":
possibles = possibles.replace(ch, " ")
possibles = possibles.split()
for name in possibles:
if name in self.name_cache:
return self.name_cache[name]
@@ -465,8 +437,15 @@ class LinksAwakeningWorld(World):
# If the item name contains "sword", use a sword icon, etc
# Otherwise, use a cute letter as the icon
elif self.options.foreign_item_icons == 'guess_by_name':
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item)
loc.ladxr_item.custom_item_name = loc.item.name
else:
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item.name)
if loc.item.advancement:
loc.ladxr_item.item = 'PIECE_OF_POWER'
else:
loc.ladxr_item.item = 'GUARDIAN_ACORN'
loc.ladxr_item.custom_item_name = loc.item.name
if loc.item:
@@ -535,10 +514,23 @@ class LinksAwakeningWorld(World):
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"
"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",
"hard_mode",
"overworld",
]
# use the default behaviour to grab options

View File

@@ -0,0 +1,28 @@
BASE_ITEM_ID = 4000
BASE_LOCATION_ID = 4000
BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256
BASE_SHOP_LOCATION_ID = BASE_GROUND_LOCATION_ID + 30
BASE_REWARD_LOCATION_ID = BASE_SHOP_LOCATION_ID + 50
ENDGAME_REGIONS = [
"kazalt",
"king_nole_labyrinth_pre_door",
"king_nole_labyrinth_post_door",
"king_nole_labyrinth_exterior",
"king_nole_labyrinth_fall_from_exterior",
"king_nole_labyrinth_path_to_palace",
"king_nole_labyrinth_raft_entrance",
"king_nole_labyrinth_raft",
"king_nole_labyrinth_sacred_tree",
"king_nole_palace"
]
ENDGAME_PROGRESSION_ITEMS = [
"Gola's Nail",
"Gola's Fang",
"Gola's Horn",
"Logs",
"Snow Spikes"
]

View File

@@ -45,7 +45,7 @@ def generate_lithograph_hint(world: "LandstalkerWorld"):
words.append(item.name.split(" ")[0].upper())
if item.location.player != world.player:
# Add player name if it's not in our own world
player_name = world.multiworld.get_player_name(world.player)
player_name = world.multiworld.get_player_name(item.location.player)
words.append(player_name.upper())
world.random.shuffle(words)
hint_text += " ".join(words) + "\n"

View File

@@ -1,8 +1,7 @@
from typing import Dict, List, NamedTuple
from BaseClasses import Item, ItemClassification
BASE_ITEM_ID = 4000
from .Constants import BASE_ITEM_ID
class LandstalkerItem(Item):

View File

@@ -1,15 +1,11 @@
from typing import Dict, Optional
from BaseClasses import Location, ItemClassification, Item
from .Constants import *
from .Regions import LandstalkerRegion
from .data.item_source import ITEM_SOURCES_JSON
from .data.world_path import WORLD_PATHS_JSON
BASE_LOCATION_ID = 4000
BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256
BASE_SHOP_LOCATION_ID = BASE_GROUND_LOCATION_ID + 30
BASE_REWARD_LOCATION_ID = BASE_SHOP_LOCATION_ID + 50
class LandstalkerLocation(Location):
game: str = "Landstalker - The Treasures of King Nole"
@@ -21,10 +17,14 @@ class LandstalkerLocation(Location):
self.type_string = type_string
def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], name_to_id_table: Dict[str, int]):
def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion],
name_to_id_table: Dict[str, int], reach_kazalt_goal: bool):
# Create real locations from the data inside the corresponding JSON file
for data in ITEM_SOURCES_JSON:
region_id = data["nodeId"]
# If "Reach Kazalt" goal is enabled and location is beyond Kazalt, don't create it
if reach_kazalt_goal and region_id in ENDGAME_REGIONS:
continue
region = regions_table[region_id]
new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"])
region.locations.append(new_location)
@@ -32,6 +32,10 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n
# Create fake event locations that will be used to determine if some key regions has been visited
regions_with_entrance_checks = []
for data in WORLD_PATHS_JSON:
# If "Reach Kazalt" goal is enabled and region is beyond Kazalt, don't create any event for it since it would
# be useless anyway
if reach_kazalt_goal and data["fromId"] in ENDGAME_REGIONS:
continue
if "requiredNodes" in data:
regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]])
regions_with_entrance_checks = sorted(set(regions_with_entrance_checks))

View File

@@ -37,7 +37,8 @@ def add_path_requirements(world: "LandstalkerWorld"):
name = data["fromId"] + " -> " + data["toId"]
# Determine required items to reach this region
required_items = data["requiredItems"] if "requiredItems" in data else []
# WORLD_PATHS_JSON is shared by all Landstalker worlds, so a copy is made to prevent modifying the original
required_items = data["requiredItems"].copy() if "requiredItems" in data else []
if "itemsPlacedWhenCrossing" in data:
required_items += data["itemsPlacedWhenCrossing"]

View File

@@ -2,6 +2,7 @@ from typing import ClassVar, Set
from BaseClasses import LocationProgressType, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Constants import *
from .Hints import *
from .Items import *
from .Locations import *
@@ -87,7 +88,8 @@ class LandstalkerWorld(World):
def create_regions(self):
self.regions_table = Regions.create_regions(self)
Locations.create_locations(self.player, self.regions_table, self.location_name_to_id)
Locations.create_locations(self.player, self.regions_table, self.location_name_to_id,
self.options.goal == "reach_kazalt")
self.create_teleportation_trees()
def create_item(self, name: str, classification_override: Optional[ItemClassification] = None) -> LandstalkerItem:
@@ -109,7 +111,16 @@ class LandstalkerWorld(World):
# If item is an armor and progressive armors are enabled, transform it into a progressive armor item
if self.options.progressive_armors and "Breast" in name:
name = "Progressive Armor"
item_pool += [self.create_item(name) for _ in range(data.quantity)]
qty = data.quantity
if self.options.goal == "reach_kazalt":
# In "Reach Kazalt" goal, remove all endgame progression items that would be useless anyway
if name in ENDGAME_PROGRESSION_ITEMS:
continue
# Also reduce quantities for most filler items to let space for more EkeEke (see end of function)
if data.classification == ItemClassification.filler:
qty = int(qty * 0.8)
item_pool += [self.create_item(name) for _ in range(qty)]
# If the appropriate setting is on, place one EkeEke in one shop in every town in the game
if self.options.ensure_ekeeke_in_shops:
@@ -120,9 +131,10 @@ class LandstalkerWorld(World):
"Mercator: Shop item #1",
"Verla: Shop item #1",
"Destel: Inn item",
"Route to Lake Shrine: Greedly's shop item #1",
"Kazalt: Shop item #1"
"Route to Lake Shrine: Greedly's shop item #1"
]
if self.options.goal != "reach_kazalt":
shops_to_fill.append("Kazalt: Shop item #1")
for location_name in shops_to_fill:
self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item("EkeEke"))

View File

@@ -73,6 +73,22 @@ WORLD_NODES_JSON = {
"between Gumi and Ryuma"
]
},
"tibor_tree": {
"name": "Route from Gumi to Ryuma (Tibor tree)",
"hints": [
"on a route",
"in a region inhabited by bears",
"between Gumi and Ryuma"
]
},
"mercator_gate_tree": {
"name": "Route from Gumi to Ryuma (Mercator gate tree)",
"hints": [
"on a route",
"in a region inhabited by bears",
"between Gumi and Ryuma"
]
},
"tibor": {
"name": "Tibor",
"hints": [
@@ -223,6 +239,13 @@ WORLD_NODES_JSON = {
"in the infamous Greenmaze"
]
},
"greenmaze_post_whistle_tree": {
"name": "Greenmaze (post-whistle tree)",
"hints": [
"among the trees",
"in the infamous Greenmaze"
]
},
"verla_shore": {
"name": "Verla shore",
"hints": [
@@ -230,6 +253,13 @@ WORLD_NODES_JSON = {
"near the town of Verla"
]
},
"verla_shore_tree": {
"name": "Verla shore tree",
"hints": [
"on a route",
"near the town of Verla"
]
},
"verla_shore_cliff": {
"name": "Verla shore cliff (accessible from Verla Mines)",
"hints": [
@@ -326,6 +356,12 @@ WORLD_NODES_JSON = {
"in a mountainous area"
]
},
"mountainous_area_tree": {
"name": "Mountainous Area tree",
"hints": [
"in a mountainous area"
]
},
"king_nole_cave": {
"name": "King Nole's Cave",
"hints": [

View File

@@ -54,6 +54,16 @@ WORLD_PATHS_JSON = [
"toId": "ryuma",
"twoWay": True
},
{
"fromId": "route_gumi_ryuma",
"toId": "tibor_tree",
"twoWay": True
},
{
"fromId": "route_gumi_ryuma",
"toId": "mercator_gate_tree",
"twoWay": True
},
{
"fromId": "ryuma",
"toId": "ryuma_after_thieves_hideout",
@@ -211,6 +221,11 @@ WORLD_PATHS_JSON = [
],
"twoWay": True
},
{
"fromId": "greenmaze_post_whistle",
"toId": "greenmaze_post_whistle_tree",
"twoWay": True
},
{
"fromId": "greenmaze_post_whistle",
"toId": "route_massan_gumi"
@@ -253,6 +268,11 @@ WORLD_PATHS_JSON = [
"fromId": "verla_shore_cliff",
"toId": "verla_shore"
},
{
"fromId": "verla_shore",
"toId": "verla_shore_tree",
"twoWay": True
},
{
"fromId": "verla_shore",
"toId": "mir_tower_sector",
@@ -316,6 +336,11 @@ WORLD_PATHS_JSON = [
"Axe Magic"
]
},
{
"fromId": "mountainous_area",
"toId": "mountainous_area_tree",
"twoWay": True
},
{
"fromId": "mountainous_area",
"toId": "route_lake_shrine_cliff",

View File

@@ -57,7 +57,9 @@ WORLD_REGIONS_JSON = [
"name": "Route between Gumi and Ryuma",
"canBeHintedAsRequired": False,
"nodeIds": [
"route_gumi_ryuma"
"route_gumi_ryuma",
"tibor_tree",
"mercator_gate_tree"
]
},
{
@@ -157,7 +159,8 @@ WORLD_REGIONS_JSON = [
"hintName": "in Greenmaze",
"nodeIds": [
"greenmaze_pre_whistle",
"greenmaze_post_whistle"
"greenmaze_post_whistle",
"greenmaze_post_whistle_tree"
]
},
{
@@ -165,7 +168,8 @@ WORLD_REGIONS_JSON = [
"canBeHintedAsRequired": False,
"nodeIds": [
"verla_shore",
"verla_shore_cliff"
"verla_shore_cliff",
"verla_shore_tree"
]
},
{
@@ -244,7 +248,8 @@ WORLD_REGIONS_JSON = [
"name": "Mountainous Area",
"hintName": "in the mountainous area",
"nodeIds": [
"mountainous_area"
"mountainous_area",
"mountainous_area_tree"
]
},
{

View File

@@ -8,19 +8,19 @@ WORLD_TELEPORT_TREES_JSON = [
{
"name": "Tibor tree",
"treeMapId": 534,
"nodeId": "route_gumi_ryuma"
"nodeId": "tibor_tree"
}
],
[
{
"name": "Mercator front gate tree",
"treeMapId": 539,
"nodeId": "route_gumi_ryuma"
"nodeId": "mercator_gate_tree"
},
{
"name": "Verla shore tree",
"treeMapId": 537,
"nodeId": "verla_shore"
"nodeId": "verla_shore_tree"
}
],
[
@@ -44,7 +44,7 @@ WORLD_TELEPORT_TREES_JSON = [
{
"name": "Mountainous area tree",
"treeMapId": 535,
"nodeId": "mountainous_area"
"nodeId": "mountainous_area_tree"
}
],
[
@@ -56,7 +56,7 @@ WORLD_TELEPORT_TREES_JSON = [
{
"name": "Greenmaze end tree",
"treeMapId": 511,
"nodeId": "greenmaze_post_whistle"
"nodeId": "greenmaze_post_whistle_tree"
}
]
]

View File

@@ -50,8 +50,8 @@ class TestVerifyItemName(L2ACTestBase):
def test_verify_item_name(self) -> None:
self.assertRaisesRegex(Exception,
"Item The car blade from option CustomItemPool\\(The car blade: 2\\) is not a "
"valid item name from Lufia II Ancient Cave\\. Did you mean 'Dekar blade'",
"Item 'The car blade' from option 'CustomItemPool\\(The car blade: 2\\)' is not a "
"valid item name from 'Lufia II Ancient Cave'\\. Did you mean 'Dekar blade'",
lambda: handle_option(Namespace(game="Lufia II Ancient Cave", name="Player"),
self.options, "custom_item_pool", CustomItemPool,
PlandoOptions(0)))

View File

@@ -381,7 +381,7 @@ class MessengerWorld(World):
return
# the messenger client calls into AP with specific args, so check the out path matches what the client sends
out_path = output_path(multiworld.get_out_file_name_base(1) + ".aptm")
if "The Messenger\\Archipelago\\output" not in out_path:
if "Messenger\\Archipelago\\output" not in out_path:
return
import orjson
data = {

View File

@@ -53,6 +53,7 @@ def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int)
filler_pool = weights.copy()
if not world.options.bad_effects:
del filler_pool["Trap"]
del filler_pool["Greed Die"]
return world.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
@@ -114,7 +115,7 @@ item_table: Dict[str, ItemData] = {
"Secret Potion": ItemData(110024, "Items", ItemClassification.filler),
"Powder Pouch": ItemData(110025, "Items", ItemClassification.filler),
"Chaos Die": ItemData(110026, "Items", ItemClassification.filler),
"Greed Die": ItemData(110027, "Items", ItemClassification.filler),
"Greed Die": ItemData(110027, "Items", ItemClassification.trap),
"Kammi": ItemData(110028, "Items", ItemClassification.filler, 1),
"Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1),
"Sädekivi": ItemData(110030, "Items", ItemClassification.filler),

View File

@@ -397,13 +397,13 @@ def _init() -> None:
label = []
for word in map_name[4:].split("_"):
# 1F, B1F, 2R, etc.
re_match = re.match("^B?\d+[FRP]$", word)
re_match = re.match(r"^B?\d+[FRP]$", word)
if re_match:
label.append(word)
continue
# Route 103, Hall 1, House 5, etc.
re_match = re.match("^([A-Z]+)(\d+)$", word)
re_match = re.match(r"^([A-Z]+)(\d+)$", word)
if re_match:
label.append(re_match.group(1).capitalize())
label.append(re_match.group(2).lstrip("0"))
@@ -1459,9 +1459,6 @@ def _init() -> None:
for warp, destination in extracted_data["warps"].items():
data.warp_map[warp] = None if destination == "" else destination
if encoded_warp not in data.warp_map:
data.warp_map[encoded_warp] = None
# Create trainer data
for i, trainer_json in enumerate(extracted_data["trainers"]):
party_json = trainer_json["party"]

View File

@@ -416,13 +416,16 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
# Dewford Town
entrance = get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH")
set_rule(
get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"),
entrance,
lambda state:
state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
and state.has("EVENT_TALK_TO_MR_STONE", world.player)
and state.has("EVENT_DELIVER_LETTER", world.player)
)
world.multiworld.register_indirect_condition(
get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance)
set_rule(
get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"),
lambda state:
@@ -451,14 +454,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
# Route 109
entrance = get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN")
set_rule(
get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"),
entrance,
lambda state:
state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player)
and state.has("EVENT_TALK_TO_MR_STONE", world.player)
and state.has("EVENT_DELIVER_LETTER", world.player)
)
world.multiworld.register_indirect_condition(
get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance)
set_rule(
get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"),
hm_rules["HM03 Surf"]

View File

@@ -749,8 +749,8 @@ location_data = [
LocationData("Cinnabar Gym", "Super Nerd 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM"], EventFlag(372), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Burglar 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM"], EventFlag(371), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM"], EventFlag(370), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 5", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Burglar 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity),
LocationData("Celadon Prize Corner", "Item Prize 1", "TM23 Dragon Rage", rom_addresses["Prize_Item_A"], EventFlag(0x69a), inclusion=prizesanity),
LocationData("Celadon Prize Corner", "Item Prize 2", "TM15 Hyper Beam", rom_addresses["Prize_Item_B"], EventFlag(0x69B), inclusion=prizesanity),

View File

@@ -1718,7 +1718,7 @@ def create_regions(world):
connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player))
connect(multiworld, player, "Vermilion City", "Route 11")
connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player))
connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-W", "Route 11-E")
connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True)

View File

@@ -1,17 +1,25 @@
import os
import json
import os
import pkgutil
from datetime import datetime
def load_data_file(*args) -> dict:
fname = "/".join(["data", *args])
return json.loads(pkgutil.get_data(__name__, fname).decode())
def relative_years_from_today(dt2: datetime) -> int:
today = datetime.now()
years = today.year - dt2.year
if today.month < dt2.month or (today.month == dt2.month and today.day < dt2.day):
years -= 1
return years
location_id_offset: int = 27000
location_info = load_data_file("locations.json")
location_name_to_id = {name: location_id_offset + index \
for index, name in enumerate(location_info["all_locations"])}
location_name_to_id = {name: location_id_offset + index for index, name in enumerate(location_info["all_locations"])}
exclusion_info = load_data_file("excluded_locations.json")
region_info = load_data_file("regions.json")
years_since_sep_30_1980 = relative_years_from_today(datetime.fromisoformat("1980-09-30"))

View File

@@ -1,132 +1,198 @@
import enum
from typing import NamedTuple, Optional
from BaseClasses import Item, ItemClassification
import typing
from . import Constants
class ShiversItem(Item):
game: str = "Shivers"
class ItemData(typing.NamedTuple):
code: int
type: str
class ItemType(enum.Enum):
POT = "pot"
POT_COMPLETE = "pot-complete"
POT_DUPLICATE = "pot-duplicate"
POT_COMPLETE_DUPLICATE = "pot-complete-duplicate"
KEY = "key"
KEY_OPTIONAL = "key-optional"
ABILITY = "ability"
FILLER = "filler"
IXUPI_AVAILABILITY = "ixupi-availability"
GOAL = "goal"
class ItemData(NamedTuple):
code: Optional[int]
type: ItemType
classification: ItemClassification = ItemClassification.progression
SHIVERS_ITEM_ID_OFFSET = 27000
item_table = {
#Pot Pieces
"Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, "pot"),
"Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, "pot"),
"Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, "pot"),
"Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, "pot"),
"Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, "pot"),
"Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, "pot"),
"Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, "pot"),
"Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, "pot"),
"Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, "pot"),
"Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, "pot"),
"Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, "pot"),
"Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, "pot"),
"Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, "pot"),
"Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, "pot"),
"Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, "pot"),
"Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, "pot"),
"Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, "pot"),
"Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"),
"Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"),
"Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"),
"Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "pot_type2"),
"Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "pot_type2"),
"Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "pot_type2"),
"Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "pot_type2"),
"Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "pot_type2"),
"Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "pot_type2"),
"Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "pot_type2"),
"Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "pot_type2"),
"Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "pot_type2"),
"Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "pot_type2"),
#Keys
"Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"),
"Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"),
"Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"),
"Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"),
"Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"),
"Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"),
"Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"),
"Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"),
"Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"),
"Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key"),
"Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, "key"),
"Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, "key"),
"Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, "key"),
"Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, "key"),
"Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, "key"),
"Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, "key"),
"Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, "key"),
"Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, "key"),
"Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, "key"),
"Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, "key-optional"),
#Abilities
"Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"),
#Event Items
"Victory": ItemData(SHIVERS_ITEM_ID_OFFSET + 60, "victory"),
#Duplicate pot pieces for fill_Restrictive
"Water Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 70, "potduplicate"),
"Wax Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 71, "potduplicate"),
"Ash Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 72, "potduplicate"),
"Oil Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 73, "potduplicate"),
"Cloth Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 74, "potduplicate"),
"Wood Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 75, "potduplicate"),
"Crystal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 76, "potduplicate"),
"Lightning Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 77, "potduplicate"),
"Sand Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 78, "potduplicate"),
"Metal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 79, "potduplicate"),
"Water Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 80, "potduplicate"),
"Wax Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 81, "potduplicate"),
"Ash Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 82, "potduplicate"),
"Oil Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 83, "potduplicate"),
"Cloth Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 84, "potduplicate"),
"Wood Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 85, "potduplicate"),
"Crystal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 86, "potduplicate"),
"Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"),
"Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"),
"Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"),
"Water Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 140, "potduplicate_type2"),
"Wax Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 141, "potduplicate_type2"),
"Ash Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 142, "potduplicate_type2"),
"Oil Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 143, "potduplicate_type2"),
"Cloth Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 144, "potduplicate_type2"),
"Wood Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 145, "potduplicate_type2"),
"Crystal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 146, "potduplicate_type2"),
"Lightning Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 147, "potduplicate_type2"),
"Sand Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 148, "potduplicate_type2"),
"Metal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 149, "potduplicate_type2"),
#Filler
"Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"),
"Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, "filler", ItemClassification.filler),
"Water Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 92, "filler2", ItemClassification.filler),
"Wax Always Available in Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 93, "filler2", ItemClassification.filler),
"Wax Always Available in Anansi Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 94, "filler2", ItemClassification.filler),
"Wax Always Available in Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 95, "filler2", ItemClassification.filler),
"Ash Always Available in Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 96, "filler2", ItemClassification.filler),
"Ash Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 97, "filler2", ItemClassification.filler),
"Oil Always Available in Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 98, "filler2", ItemClassification.filler),
"Cloth Always Available in Egypt": ItemData(SHIVERS_ITEM_ID_OFFSET + 99, "filler2", ItemClassification.filler),
"Cloth Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 100, "filler2", ItemClassification.filler),
"Wood Always Available in Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 101, "filler2", ItemClassification.filler),
"Wood Always Available in Blue Maze": ItemData(SHIVERS_ITEM_ID_OFFSET + 102, "filler2", ItemClassification.filler),
"Wood Always Available in Pegasus Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 103, "filler2", ItemClassification.filler),
"Wood Always Available in Gods Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 104, "filler2", ItemClassification.filler),
"Crystal Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 105, "filler2", ItemClassification.filler),
"Crystal Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 106, "filler2", ItemClassification.filler),
"Sand Always Available in Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 107, "filler2", ItemClassification.filler),
"Sand Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 108, "filler2", ItemClassification.filler),
"Metal Always Available in Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 109, "filler2", ItemClassification.filler),
"Metal Always Available in Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 110, "filler2", ItemClassification.filler),
"Metal Always Available in Prehistoric": ItemData(SHIVERS_ITEM_ID_OFFSET + 111, "filler2", ItemClassification.filler),
"Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, "filler3", ItemClassification.filler)
# To allow for an item with a name that changes over time (once a year)
# while keeping the id unique we can generate a small range of them.
goal_items = {
f"Mt. Pleasant Tribune: {Constants.years_since_sep_30_1980 + year_offset} year Old Mystery Solved!": ItemData(
SHIVERS_ITEM_ID_OFFSET + 100 + Constants.years_since_sep_30_1980 + year_offset, ItemType.GOAL
) for year_offset in range(-1, 2)
}
item_table = {
# Pot Pieces
"Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, ItemType.POT),
"Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, ItemType.POT),
"Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, ItemType.POT),
"Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, ItemType.POT),
"Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, ItemType.POT),
"Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, ItemType.POT),
"Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, ItemType.POT),
"Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, ItemType.POT),
"Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, ItemType.POT),
"Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, ItemType.POT),
"Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, ItemType.POT),
"Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, ItemType.POT),
"Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, ItemType.POT),
"Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, ItemType.POT),
"Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, ItemType.POT),
"Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, ItemType.POT),
"Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, ItemType.POT),
"Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, ItemType.POT),
"Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, ItemType.POT),
"Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, ItemType.POT),
"Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, ItemType.POT_COMPLETE),
"Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, ItemType.POT_COMPLETE),
"Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, ItemType.POT_COMPLETE),
"Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, ItemType.POT_COMPLETE),
"Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, ItemType.POT_COMPLETE),
"Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, ItemType.POT_COMPLETE),
"Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, ItemType.POT_COMPLETE),
"Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, ItemType.POT_COMPLETE),
"Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, ItemType.POT_COMPLETE),
"Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, ItemType.POT_COMPLETE),
# Keys
"Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, ItemType.KEY),
"Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, ItemType.KEY),
"Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, ItemType.KEY),
"Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, ItemType.KEY),
"Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, ItemType.KEY),
"Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, ItemType.KEY),
"Key for Greenhouse": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, ItemType.KEY),
"Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, ItemType.KEY),
"Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, ItemType.KEY),
"Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, ItemType.KEY),
"Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, ItemType.KEY),
"Key for Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, ItemType.KEY),
"Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, ItemType.KEY),
"Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, ItemType.KEY),
"Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, ItemType.KEY),
"Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, ItemType.KEY),
"Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, ItemType.KEY),
"Key for Underground Lake": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, ItemType.KEY),
"Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, ItemType.KEY),
"Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, ItemType.KEY_OPTIONAL),
# Abilities
"Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, ItemType.ABILITY),
# Duplicate pot pieces for fill_Restrictive
"Water Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Wax Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Ash Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Oil Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Cloth Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Wood Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Crystal Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Lightning Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Sand Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Metal Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Water Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Wax Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Ash Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Oil Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Cloth Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Wood Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Crystal Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Lightning Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Sand Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Metal Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Water Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Wax Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Ash Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Oil Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Cloth Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Wood Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Crystal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Lightning Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Sand Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Metal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
# Filler
"Empty": ItemData(None, ItemType.FILLER, ItemClassification.filler),
"Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, ItemType.FILLER, ItemClassification.useful),
"Water Always Available in Lobby": ItemData(
SHIVERS_ITEM_ID_OFFSET + 92, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wax Always Available in Library": ItemData(
SHIVERS_ITEM_ID_OFFSET + 93, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wax Always Available in Anansi Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 94, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wax Always Available in Shaman Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 95, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Ash Always Available in Office": ItemData(
SHIVERS_ITEM_ID_OFFSET + 96, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Ash Always Available in Burial Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 97, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Oil Always Available in Prehistoric Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 98, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Cloth Always Available in Egypt": ItemData(
SHIVERS_ITEM_ID_OFFSET + 99, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Cloth Always Available in Burial Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 100, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wood Always Available in Workshop": ItemData(
SHIVERS_ITEM_ID_OFFSET + 101, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wood Always Available in Blue Maze": ItemData(
SHIVERS_ITEM_ID_OFFSET + 102, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wood Always Available in Pegasus Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 103, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Wood Always Available in Gods Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 104, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Crystal Always Available in Lobby": ItemData(
SHIVERS_ITEM_ID_OFFSET + 105, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Crystal Always Available in Ocean": ItemData(
SHIVERS_ITEM_ID_OFFSET + 106, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Sand Always Available in Greenhouse": ItemData(
SHIVERS_ITEM_ID_OFFSET + 107, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Sand Always Available in Ocean": ItemData(
SHIVERS_ITEM_ID_OFFSET + 108, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Metal Always Available in Projector Room": ItemData(
SHIVERS_ITEM_ID_OFFSET + 109, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Metal Always Available in Bedroom": ItemData(
SHIVERS_ITEM_ID_OFFSET + 110, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Metal Always Available in Prehistoric": ItemData(
SHIVERS_ITEM_ID_OFFSET + 111, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
),
"Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, ItemType.FILLER, ItemClassification.filler),
# Goal items
**goal_items
}

View File

@@ -1,6 +1,12 @@
from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions, Range
from dataclasses import dataclass
from Options import (
Choice, DefaultOnToggle, ExcludeLocations, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
PriorityLocations, Range, StartHints, StartInventory, StartLocationHints, Toggle,
)
from . import ItemType, item_table
from .Constants import location_info
class IxupiCapturesNeeded(Range):
"""
@@ -11,12 +17,13 @@ class IxupiCapturesNeeded(Range):
range_end = 10
default = 10
class LobbyAccess(Choice):
"""
Chooses how keys needed to reach the lobby are placed.
- Normal: Keys are placed anywhere
- Early: Keys are placed early
- Local: Keys are placed locally
- Local: Keys are placed locally and early
"""
display_name = "Lobby Access"
option_normal = 0
@@ -24,16 +31,19 @@ class LobbyAccess(Choice):
option_local = 2
default = 1
class PuzzleHintsRequired(DefaultOnToggle):
"""
If turned on puzzle hints/solutions will be available before the corresponding puzzle is required.
For example: The Red Door puzzle will be logically required only after access to the Beth's Address Book which gives you the solution.
For example: The Red Door puzzle will be logically required only after obtaining access to Beth's Address Book
which gives you the solution.
Turning this off allows for greater randomization.
"""
display_name = "Puzzle Hints Required"
class InformationPlaques(Toggle):
"""
Adds Information Plaques as checks.
@@ -41,12 +51,14 @@ class InformationPlaques(Toggle):
"""
display_name = "Include Information Plaques"
class FrontDoorUsable(Toggle):
"""
Adds a key to unlock the front door of the museum.
"""
display_name = "Front Door Usable"
class ElevatorsStaySolved(DefaultOnToggle):
"""
Adds elevators as checks and will remain open upon solving them.
@@ -54,12 +66,15 @@ class ElevatorsStaySolved(DefaultOnToggle):
"""
display_name = "Elevators Stay Solved"
class EarlyBeth(DefaultOnToggle):
"""
Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.
Beth's body is open at the start of the game.
This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.
"""
display_name = "Early Beth"
class EarlyLightning(Toggle):
"""
Allows lightning to be captured at any point in the game. You will still need to capture all ten Ixupi for victory.
@@ -67,6 +82,7 @@ class EarlyLightning(Toggle):
"""
display_name = "Early Lightning"
class LocationPotPieces(Choice):
"""
Chooses where pot pieces will be located within the multiworld.
@@ -78,6 +94,8 @@ class LocationPotPieces(Choice):
option_own_world = 0
option_different_world = 1
option_any_world = 2
default = 2
class FullPots(Choice):
"""
@@ -92,6 +110,13 @@ class FullPots(Choice):
option_mixed = 2
class IxupiCapturesPriority(DefaultOnToggle):
"""
Ixupi captures are set to priority locations. This forces a progression item into these locations if possible.
"""
display_name = "Ixupi Captures are Priority"
class PuzzleCollectBehavior(Choice):
"""
Defines what happens to puzzles on collect.
@@ -107,6 +132,46 @@ class PuzzleCollectBehavior(Choice):
default = 1
# Need to override the default options to remove the goal items and goal locations so that they do not show on web.
valid_item_keys = [name for name, data in item_table.items() if data.type != ItemType.GOAL and data.code is not None]
valid_location_keys = [name for name in location_info["all_locations"] if name != "Mystery Solved"]
class ShiversLocalItems(LocalItems):
__doc__ = LocalItems.__doc__
valid_keys = valid_item_keys
class ShiversNonLocalItems(NonLocalItems):
__doc__ = NonLocalItems.__doc__
valid_keys = valid_item_keys
class ShiversStartInventory(StartInventory):
__doc__ = StartInventory.__doc__
valid_keys = valid_item_keys
class ShiversStartHints(StartHints):
__doc__ = StartHints.__doc__
valid_keys = valid_item_keys
class ShiversStartLocationHints(StartLocationHints):
__doc__ = StartLocationHints.__doc__
valid_keys = valid_location_keys
class ShiversExcludeLocations(ExcludeLocations):
__doc__ = ExcludeLocations.__doc__
valid_keys = valid_location_keys
class ShiversPriorityLocations(PriorityLocations):
__doc__ = PriorityLocations.__doc__
valid_keys = valid_location_keys
@dataclass
class ShiversOptions(PerGameCommonOptions):
ixupi_captures_needed: IxupiCapturesNeeded
@@ -119,4 +184,27 @@ class ShiversOptions(PerGameCommonOptions):
early_lightning: EarlyLightning
location_pot_pieces: LocationPotPieces
full_pots: FullPots
ixupi_captures_priority: IxupiCapturesPriority
puzzle_collect_behavior: PuzzleCollectBehavior
local_items: ShiversLocalItems
non_local_items: ShiversNonLocalItems
start_inventory: ShiversStartInventory
start_hints: ShiversStartHints
start_location_hints: ShiversStartLocationHints
exclude_locations: ShiversExcludeLocations
priority_locations: ShiversPriorityLocations
shivers_option_groups = [
OptionGroup(
"Item & Location Options", [
ShiversLocalItems,
ShiversNonLocalItems,
ShiversStartInventory,
ShiversStartHints,
ShiversStartLocationHints,
ShiversExcludeLocations,
ShiversPriorityLocations
], True,
),
]

View File

@@ -1,66 +1,69 @@
from typing import Dict, TYPE_CHECKING
from collections.abc import Callable
from typing import Dict, TYPE_CHECKING
from BaseClasses import CollectionState
from worlds.generic.Rules import forbid_item
from . import Constants
if TYPE_CHECKING:
from . import ShiversWorld
def water_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) or \
state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player)
return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) \
or state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player)
def wax_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) or \
state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player)
return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) \
or state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player)
def ash_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) or \
state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player)
return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) \
or state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player)
def oil_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) or \
state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player)
return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) \
or state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player)
def cloth_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) or \
state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player)
return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) \
or state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player)
def wood_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) or \
state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player)
return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) \
or state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player)
def crystal_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) or \
state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player)
return state.has_all(
{"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) \
or state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player)
def sand_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) or \
state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player)
return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) \
or state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player)
def metal_capturable(state: CollectionState, player: int) -> bool:
return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) or \
state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player)
return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) \
or state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player)
def lightning_capturable(state: CollectionState, player: int) -> bool:
return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_lightning.value) \
and (state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) or \
state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player))
def lightning_capturable(state: CollectionState, world: "ShiversWorld", player: int) -> bool:
return (first_nine_ixupi_capturable(state, player) or world.options.early_lightning) \
and (state.has_all(
{"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"},
player) or state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player))
def beths_body_available(state: CollectionState, player: int) -> bool:
return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_beth.value) \
and state.can_reach("Generator", "Region", player)
def beths_body_available(state: CollectionState, world: "ShiversWorld", player: int) -> bool:
return first_nine_ixupi_capturable(state, player) or world.options.early_beth
def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool:
@@ -71,13 +74,22 @@ def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool:
and metal_capturable(state, player)
def all_skull_dials_available(state: CollectionState, player: int) -> bool:
return state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region", player) \
and state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) \
and state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player)
def all_skull_dials_set(state: CollectionState, player: int) -> bool:
return state.has_all([
"Set Skull Dial: Prehistoric",
"Set Skull Dial: Tar River",
"Set Skull Dial: Egypt",
"Set Skull Dial: Burial",
"Set Skull Dial: Gods Room",
"Set Skull Dial: Werewolf"
], player)
def get_rules_lookup(player: int):
def completion_condition(state: CollectionState, player: int) -> bool:
return state.has(f"Mt. Pleasant Tribune: {Constants.years_since_sep_30_1980} year Old Mystery Solved!", player)
def get_rules_lookup(world: "ShiversWorld", player: int):
rules_lookup: Dict[str, Dict[str, Callable[[CollectionState], bool]]] = {
"entrances": {
"To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player),
@@ -90,48 +102,58 @@ def get_rules_lookup(player: int):
"To Workshop": lambda state: state.has("Key for Workshop", player),
"To Lobby From Office": lambda state: state.has("Key for Office", player),
"To Office From Lobby": lambda state: state.has("Key for Office", player),
"To Library From Lobby": lambda state: state.has("Key for Library Room", player),
"To Lobby From Library": lambda state: state.has("Key for Library Room", player),
"To Library From Lobby": lambda state: state.has("Key for Library", player),
"To Lobby From Library": lambda state: state.has("Key for Library", player),
"To Prehistoric From Lobby": lambda state: state.has("Key for Prehistoric Room", player),
"To Lobby From Prehistoric": lambda state: state.has("Key for Prehistoric Room", player),
"To Greenhouse": lambda state: state.has("Key for Greenhouse Room", player),
"To Greenhouse": lambda state: state.has("Key for Greenhouse", player),
"To Ocean From Prehistoric": lambda state: state.has("Key for Ocean Room", player),
"To Prehistoric From Ocean": lambda state: state.has("Key for Ocean Room", player),
"To Projector Room": lambda state: state.has("Key for Projector Room", player),
"To Generator": lambda state: state.has("Key for Generator Room", player),
"To Generator From Maintenance Tunnels": lambda state: state.has("Key for Generator Room", player),
"To Lobby From Egypt": lambda state: state.has("Key for Egypt Room", player),
"To Egypt From Lobby": lambda state: state.has("Key for Egypt Room", player),
"To Janitor Closet": lambda state: state.has("Key for Janitor Closet", player),
"To Shaman From Burial": lambda state: state.has("Key for Shaman Room", player),
"To Burial From Shaman": lambda state: state.has("Key for Shaman Room", player),
"To Norse Stone From Gods Room": lambda state: state.has("Aligned Planets", player),
"To Inventions From UFO": lambda state: state.has("Key for UFO Room", player),
"To UFO From Inventions": lambda state: state.has("Key for UFO Room", player),
"To Orrery From UFO": lambda state: state.has("Viewed Fortune", player),
"To Torture From Inventions": lambda state: state.has("Key for Torture Room", player),
"To Inventions From Torture": lambda state: state.has("Key for Torture Room", player),
"To Torture": lambda state: state.has("Key for Puzzle Room", player),
"To Puzzle Room Mastermind From Torture": lambda state: state.has("Key for Puzzle Room", player),
"To Bedroom": lambda state: state.has("Key for Bedroom", player),
"To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake Room", player),
"To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake Room", player),
"To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake", player),
"To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake", player),
"To Outside From Lobby": lambda state: state.has("Key for Front Door", player),
"To Lobby From Outside": lambda state: state.has("Key for Front Door", player),
"To Maintenance Tunnels From Theater Back Hallways": lambda state: state.has("Crawling", player),
"To Maintenance Tunnels From Theater Back Hallway": lambda state: state.has("Crawling", player),
"To Blue Maze From Egypt": lambda state: state.has("Crawling", player),
"To Egypt From Blue Maze": lambda state: state.has("Crawling", player),
"To Lobby From Tar River": lambda state: (state.has("Crawling", player) and oil_capturable(state, player)),
"To Tar River From Lobby": lambda state: (state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach("Tar River", "Region", player)),
"To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player),
"To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player),
"To Slide Room": lambda state: all_skull_dials_available(state, player),
"To Lobby From Slide Room": lambda state: beths_body_available(state, player),
"To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player)
"To Lobby From Tar River": lambda state: state.has("Crawling", player) and oil_capturable(state, player),
"To Tar River From Lobby": lambda state: state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach_region("Tar River", player),
"To Burial From Egypt": lambda state: state.can_reach_region("Egypt", player),
"To Gods Room From Anansi": lambda state: state.can_reach_region("Gods Room", player),
"To Slide Room": lambda state: all_skull_dials_set(state, player),
"To Lobby From Slide Room": lambda state: state.has("Lost Your Head", player),
"To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player),
"To Victory": lambda state: (
(water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player)
+ oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player)
+ crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player)
+ lightning_capturable(state, world, player)) >= world.options.ixupi_captures_needed.value
)
},
"locations_required": {
"Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player),
"Accessible: Storage: Janitor Closet": lambda state: cloth_capturable(state, player),
"Accessible: Storage: Tar River": lambda state: oil_capturable(state, player),
"Accessible: Storage: Theater": lambda state: state.can_reach("Projector Room", "Region", player),
"Accessible: Storage: Slide": lambda state: beths_body_available(state, player) and state.can_reach("Slide Room", "Region", player),
"Puzzle Solved Anansi Music Box": lambda state: state.has("Set Song", player),
"Storage: Anansi Music Box": lambda state: state.has("Set Song", player),
"Storage: Clock Tower": lambda state: state.has("Set Time", player),
"Storage: Janitor Closet": lambda state: cloth_capturable(state, player),
"Storage: Tar River": lambda state: oil_capturable(state, player),
"Storage: Theater": lambda state: state.has("Viewed Theater Movie", player),
"Storage: Slide": lambda state: state.has("Lost Your Head", player) and state.can_reach_region("Slide Room", player),
"Ixupi Captured Water": lambda state: water_capturable(state, player),
"Ixupi Captured Wax": lambda state: wax_capturable(state, player),
"Ixupi Captured Ash": lambda state: ash_capturable(state, player),
@@ -141,32 +163,28 @@ def get_rules_lookup(player: int):
"Ixupi Captured Crystal": lambda state: crystal_capturable(state, player),
"Ixupi Captured Sand": lambda state: sand_capturable(state, player),
"Ixupi Captured Metal": lambda state: metal_capturable(state, player),
"Final Riddle: Planets Aligned": lambda state: state.can_reach("Fortune Teller", "Region", player),
"Final Riddle: Norse God Stone Message": lambda state: (state.can_reach("Fortune Teller", "Region", player) and state.can_reach("UFO", "Region", player)),
"Final Riddle: Beth's Body Page 17": lambda state: beths_body_available(state, player),
"Final Riddle: Guillotine Dropped": lambda state: beths_body_available(state, player),
"Puzzle Solved Skull Dial Door": lambda state: all_skull_dials_available(state, player),
},
"locations_puzzle_hints": {
"Puzzle Solved Clock Tower Door": lambda state: state.can_reach("Three Floor Elevator", "Region", player),
"Puzzle Solved Clock Chains": lambda state: state.can_reach("Bedroom", "Region", player),
"Puzzle Solved Shaman Drums": lambda state: state.can_reach("Clock Tower", "Region", player),
"Puzzle Solved Red Door": lambda state: state.can_reach("Maintenance Tunnels", "Region", player),
"Puzzle Solved UFO Symbols": lambda state: state.can_reach("Library", "Region", player),
"Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player),
"Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player),
"Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player),
"Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
},
"Puzzle Solved Skull Dial Door": lambda state: all_skull_dials_set(state, player),
},
"puzzle_hints_required": {
"Puzzle Solved Clock Tower Door": lambda state: state.can_reach_region("Three Floor Elevator", player),
"Puzzle Solved Shaman Drums": lambda state: state.can_reach_region("Clock Tower", player),
"Puzzle Solved Red Door": lambda state: state.can_reach_region("Maintenance Tunnels", player),
"Puzzle Solved UFO Symbols": lambda state: state.can_reach_region("Library", player),
"Storage: UFO": lambda state: state.can_reach_region("Library", player),
"Puzzle Solved Maze Door": lambda state: state.has("Viewed Theater Movie", player),
"Puzzle Solved Theater Door": lambda state: state.has("Viewed Egyptian Hieroglyphics Explained", player),
"Puzzle Solved Columns of RA": lambda state: state.has("Viewed Egyptian Hieroglyphics Explained", player),
"Puzzle Solved Atlantis": lambda state: state.can_reach_region("Office", player),
},
"elevators": {
"Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player))
and state.has("Key for Office Elevator", player)),
"Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)),
"Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
and state.has("Key for Three Floor Elevator", player))
},
"Puzzle Solved Office Elevator": lambda state: (state.can_reach_region("Underground Lake", player) or state.can_reach_region("Office", player))
and state.has("Key for Office Elevator", player),
"Puzzle Solved Bedroom Elevator": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player),
"Puzzle Solved Three Floor Elevator": lambda state: (state.can_reach_region("Maintenance Tunnels", player) or state.can_reach_region("Blue Maze", player))
and state.has("Key for Three Floor Elevator", player)
},
"lightning": {
"Ixupi Captured Lightning": lambda state: lightning_capturable(state, player)
"Ixupi Captured Lightning": lambda state: lightning_capturable(state, world, player)
}
}
return rules_lookup
@@ -176,69 +194,128 @@ def set_rules(world: "ShiversWorld") -> None:
multiworld = world.multiworld
player = world.player
rules_lookup = get_rules_lookup(player)
rules_lookup = get_rules_lookup(world, player)
# Set required entrance rules
for entrance_name, rule in rules_lookup["entrances"].items():
multiworld.get_entrance(entrance_name, player).access_rule = rule
world.get_entrance(entrance_name).access_rule = rule
world.get_region("Clock Tower Staircase").connect(
world.get_region("Clock Chains"),
"To Clock Chains From Clock Tower Staircase",
lambda state: state.can_reach_region("Bedroom", player) if world.options.puzzle_hints_required.value else True
)
world.get_region("Generator").connect(
world.get_region("Beth's Body"),
"To Beth's Body From Generator",
lambda state: beths_body_available(state, world, player) and (
(state.has("Viewed Norse Stone", player) and state.can_reach_region("Theater", player))
if world.options.puzzle_hints_required.value else True
)
)
world.get_region("Torture").connect(
world.get_region("Guillotine"),
"To Guillotine From Torture",
lambda state: state.has("Viewed Page 17", player) and (
state.has("Viewed Egyptian Hieroglyphics Explained", player)
if world.options.puzzle_hints_required.value else True
)
)
# Set required location rules
for location_name, rule in rules_lookup["locations_required"].items():
multiworld.get_location(location_name, player).access_rule = rule
world.get_location(location_name).access_rule = rule
world.get_location("Jukebox").access_rule = lambda state: (
state.can_reach_region("Clock Tower", player) and (
state.can_reach_region("Anansi", player)
if world.options.puzzle_hints_required.value else True
)
)
# Set option location rules
if world.options.puzzle_hints_required.value:
for location_name, rule in rules_lookup["locations_puzzle_hints"].items():
multiworld.get_location(location_name, player).access_rule = rule
for location_name, rule in rules_lookup["puzzle_hints_required"].items():
world.get_location(location_name).access_rule = rule
world.get_entrance("To Theater From Lobby").access_rule = lambda state: state.has(
"Viewed Egyptian Hieroglyphics Explained", player
)
world.get_entrance("To Clock Tower Staircase From Theater Back Hallway").access_rule = lambda state: state.can_reach_region("Three Floor Elevator", player)
multiworld.register_indirect_condition(
world.get_region("Three Floor Elevator"),
world.get_entrance("To Clock Tower Staircase From Theater Back Hallway")
)
world.get_entrance("To Gods Room From Shaman").access_rule = lambda state: state.can_reach_region(
"Clock Tower", player
)
multiworld.register_indirect_condition(
world.get_region("Clock Tower"), world.get_entrance("To Gods Room From Shaman")
)
world.get_entrance("To Anansi From Gods Room").access_rule = lambda state: state.can_reach_region(
"Maintenance Tunnels", player
)
multiworld.register_indirect_condition(
world.get_region("Maintenance Tunnels"), world.get_entrance("To Anansi From Gods Room")
)
world.get_entrance("To Maze From Maze Staircase").access_rule = lambda \
state: state.can_reach_region("Projector Room", player)
multiworld.register_indirect_condition(
world.get_region("Projector Room"), world.get_entrance("To Maze From Maze Staircase")
)
multiworld.register_indirect_condition(
world.get_region("Bedroom"), world.get_entrance("To Clock Chains From Clock Tower Staircase")
)
multiworld.register_indirect_condition(
world.get_region("Theater"), world.get_entrance("To Beth's Body From Generator")
)
if world.options.elevators_stay_solved.value:
for location_name, rule in rules_lookup["elevators"].items():
multiworld.get_location(location_name, player).access_rule = rule
world.get_location(location_name).access_rule = rule
if world.options.early_lightning.value:
for location_name, rule in rules_lookup["lightning"].items():
multiworld.get_location(location_name, player).access_rule = rule
world.get_location(location_name).access_rule = rule
# Register indirect conditions
multiworld.register_indirect_condition(world.get_region("Burial"), world.get_entrance("To Slide Room"))
multiworld.register_indirect_condition(world.get_region("Egypt"), world.get_entrance("To Slide Room"))
multiworld.register_indirect_condition(world.get_region("Gods Room"), world.get_entrance("To Slide Room"))
multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Slide Room"))
multiworld.register_indirect_condition(world.get_region("Tar River"), world.get_entrance("To Slide Room"))
multiworld.register_indirect_condition(world.get_region("Werewolf"), world.get_entrance("To Slide Room"))
multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Tar River From Lobby"))
# forbid cloth in janitor closet and oil in tar river
forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player)
forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player)
forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Complete DUPE", player)
forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player)
forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player)
forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Complete DUPE", player)
forbid_item(world.get_location("Storage: Janitor Closet"), "Cloth Pot Bottom DUPE", player)
forbid_item(world.get_location("Storage: Janitor Closet"), "Cloth Pot Top DUPE", player)
forbid_item(world.get_location("Storage: Janitor Closet"), "Cloth Pot Complete DUPE", player)
forbid_item(world.get_location("Storage: Tar River"), "Oil Pot Bottom DUPE", player)
forbid_item(world.get_location("Storage: Tar River"), "Oil Pot Top DUPE", player)
forbid_item(world.get_location("Storage: Tar River"), "Oil Pot Complete DUPE", player)
# Filler Item Forbids
forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player)
forbid_item(multiworld.get_location("Ixupi Captured Water", player), "Water Always Available in Lobby", player)
forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Library", player)
forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Anansi Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Shaman Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Office", player)
forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Burial Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Oil", player), "Oil Always Available in Prehistoric Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Egypt", player)
forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Burial Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Workshop", player)
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Blue Maze", player)
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Pegasus Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Gods Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Lobby", player)
forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Ocean", player)
forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Plants Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Ocean", player)
forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Projector Room", player)
forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Bedroom", player)
forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player)
forbid_item(world.get_location("Puzzle Solved Lyre"), "Easier Lyre", player)
forbid_item(world.get_location("Ixupi Captured Water"), "Water Always Available in Lobby", player)
forbid_item(world.get_location("Ixupi Captured Wax"), "Wax Always Available in Library", player)
forbid_item(world.get_location("Ixupi Captured Wax"), "Wax Always Available in Anansi Room", player)
forbid_item(world.get_location("Ixupi Captured Wax"), "Wax Always Available in Shaman Room", player)
forbid_item(world.get_location("Ixupi Captured Ash"), "Ash Always Available in Office", player)
forbid_item(world.get_location("Ixupi Captured Ash"), "Ash Always Available in Burial Room", player)
forbid_item(world.get_location("Ixupi Captured Oil"), "Oil Always Available in Prehistoric Room", player)
forbid_item(world.get_location("Ixupi Captured Cloth"), "Cloth Always Available in Egypt", player)
forbid_item(world.get_location("Ixupi Captured Cloth"), "Cloth Always Available in Burial Room", player)
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Workshop", player)
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Blue Maze", player)
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Pegasus Room", player)
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Gods Room", player)
forbid_item(world.get_location("Ixupi Captured Crystal"), "Crystal Always Available in Lobby", player)
forbid_item(world.get_location("Ixupi Captured Crystal"), "Crystal Always Available in Ocean", player)
forbid_item(world.get_location("Ixupi Captured Sand"), "Sand Always Available in Plants Room", player)
forbid_item(world.get_location("Ixupi Captured Sand"), "Sand Always Available in Ocean", player)
forbid_item(world.get_location("Ixupi Captured Metal"), "Metal Always Available in Projector Room", player)
forbid_item(world.get_location("Ixupi Captured Metal"), "Metal Always Available in Bedroom", player)
forbid_item(world.get_location("Ixupi Captured Metal"), "Metal Always Available in Prehistoric", player)
# Set completion condition
multiworld.completion_condition[player] = lambda state: ((
water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player) \
+ oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player) \
+ crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player) \
+ lightning_capturable(state, player)) >= world.options.ixupi_captures_needed.value)
multiworld.completion_condition[player] = lambda state: completion_condition(state, player)

View File

@@ -1,11 +1,12 @@
from typing import List
from .Items import item_table, ShiversItem
from .Rules import set_rules
from BaseClasses import Item, Tutorial, Region, Location
from typing import Dict, List, Optional
from BaseClasses import Item, ItemClassification, Location, Region, Tutorial
from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World
from . import Constants, Rules
from .Options import ShiversOptions
from .Items import ItemType, SHIVERS_ITEM_ID_OFFSET, ShiversItem, item_table
from .Options import ShiversOptions, shivers_option_groups
from .Rules import set_rules
class ShiversWeb(WebWorld):
@@ -15,12 +16,15 @@ class ShiversWeb(WebWorld):
"English",
"setup_en.md",
"setup/en",
["GodlFire", "Mathx2"]
["GodlFire", "Cynbel_Terreus"]
)]
option_groups = shivers_option_groups
class ShiversWorld(World):
"""
Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual.
Shivers is a horror themed point and click adventure.
Explore the mysteries of Windlenot's Museum of the Strange and Unusual.
"""
game = "Shivers"
@@ -28,24 +32,41 @@ class ShiversWorld(World):
web = ShiversWeb()
options_dataclass = ShiversOptions
options: ShiversOptions
set_rules = set_rules
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = Constants.location_name_to_id
shivers_item_id_offset = 27000
storage_placements = []
pot_completed_list: List[int]
def generate_early(self):
self.pot_completed_list = []
# Pot piece shuffle location:
if self.options.location_pot_pieces == "own_world":
self.options.local_items.value |= {name for name, data in item_table.items() if
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
elif self.options.location_pot_pieces == "different_world":
self.options.non_local_items.value |= {name for name, data in item_table.items() if
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
# Ixupi captures priority locations:
if self.options.ixupi_captures_priority:
self.options.priority_locations.value |= (
{name for name in self.location_names if name.startswith('Ixupi Captured')}
)
def create_item(self, name: str) -> Item:
data = item_table[name]
return ShiversItem(name, data.classification, data.code, self.player)
def create_event(self, region_name: str, event_name: str) -> None:
region = self.multiworld.get_region(region_name, self.player)
loc = ShiversLocation(self.player, event_name, None, region)
loc.place_locked_item(self.create_event_item(event_name))
def create_event_location(self, region_name: str, location_name: str, event_name: Optional[str] = None) -> None:
region = self.get_region(region_name)
loc = ShiversLocation(self.player, location_name, None, region)
if event_name is not None:
loc.place_locked_item(ShiversItem(event_name, ItemClassification.progression, None, self.player))
else:
loc.place_locked_item(ShiversItem(location_name, ItemClassification.progression, None, self.player))
loc.show_in_spoiler = False
region.locations.append(loc)
def create_regions(self) -> None:
@@ -56,162 +77,185 @@ class ShiversWorld(World):
for exit_name in exits:
r.create_exit(exit_name)
# Bind mandatory connections
for entr_name, region_name in Constants.region_info["mandatory_connections"]:
e = self.multiworld.get_entrance(entr_name, self.player)
r = self.multiworld.get_region(region_name, self.player)
e = self.get_entrance(entr_name)
r = self.get_region(region_name)
e.connect(r)
# Locations
# Build exclusion list
self.removed_locations = set()
removed_locations = set()
if not self.options.include_information_plaques:
self.removed_locations.update(Constants.exclusion_info["plaques"])
removed_locations.update(Constants.exclusion_info["plaques"])
if not self.options.elevators_stay_solved:
self.removed_locations.update(Constants.exclusion_info["elevators"])
removed_locations.update(Constants.exclusion_info["elevators"])
if not self.options.early_lightning:
self.removed_locations.update(Constants.exclusion_info["lightning"])
removed_locations.update(Constants.exclusion_info["lightning"])
# Add locations
for region_name, locations in Constants.location_info["locations_by_region"].items():
region = self.multiworld.get_region(region_name, self.player)
region = self.get_region(region_name)
for loc_name in locations:
if loc_name not in self.removed_locations:
if loc_name not in removed_locations:
loc = ShiversLocation(self.player, loc_name, self.location_name_to_id.get(loc_name, None), region)
region.locations.append(loc)
self.create_event_location("Prehistoric", "Set Skull Dial: Prehistoric")
self.create_event_location("Tar River", "Set Skull Dial: Tar River")
self.create_event_location("Egypt", "Set Skull Dial: Egypt")
self.create_event_location("Burial", "Set Skull Dial: Burial")
self.create_event_location("Gods Room", "Set Skull Dial: Gods Room")
self.create_event_location("Werewolf", "Set Skull Dial: Werewolf")
self.create_event_location("Projector Room", "Viewed Theater Movie")
self.create_event_location("Clock Chains", "Clock Chains", "Set Time")
self.create_event_location("Clock Tower", "Jukebox", "Set Song")
self.create_event_location("Fortune Teller", "Viewed Fortune")
self.create_event_location("Orrery", "Orrery", "Aligned Planets")
self.create_event_location("Norse Stone", "Norse Stone", "Viewed Norse Stone")
self.create_event_location("Beth's Body", "Beth's Body", "Viewed Page 17")
self.create_event_location("Windlenot's Body", "Windlenot's Body", "Viewed Egyptian Hieroglyphics Explained")
self.create_event_location("Guillotine", "Guillotine", "Lost Your Head")
def create_items(self) -> None:
#Add items to item pool
itempool = []
# Add items to item pool
item_pool = []
for name, data in item_table.items():
if data.type in {"key", "ability", "filler2"}:
itempool.append(self.create_item(name))
if data.type in [ItemType.KEY, ItemType.ABILITY, ItemType.IXUPI_AVAILABILITY]:
item_pool.append(self.create_item(name))
# Pot pieces/Completed/Mixed:
for i in range(10):
if self.options.full_pots == "pieces":
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i]))
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i]))
elif self.options.full_pots == "complete":
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i]))
else:
# Roll for if pieces or a complete pot will be used.
# Pot Pieces
if self.options.full_pots == "pieces":
item_pool += [self.create_item(name) for name, data in item_table.items() if data.type == ItemType.POT]
elif self.options.full_pots == "complete":
item_pool += [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPLETE]
else:
# Roll for if pieces or a complete pot will be used.
# Pot Pieces
pieces = [self.create_item(name) for name, data in item_table.items() if data.type == ItemType.POT]
complete = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPLETE]
for i in range(10):
if self.random.randint(0, 1) == 0:
self.pot_completed_list.append(0)
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i]))
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i]))
item_pool.append(pieces[i])
item_pool.append(pieces[i + 10])
# Completed Pot
else:
self.pot_completed_list.append(1)
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i]))
item_pool.append(complete[i])
#Add Filler
itempool += [self.create_item("Easier Lyre") for i in range(9)]
# Add Easier Lyre
item_pool += [self.create_item("Easier Lyre") for _ in range(9)]
#Extra filler is random between Heals and Easier Lyre. Heals weighted 95%.
filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool)
itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)]
# Place library escape items. Choose a location to place the escape item
library_region = self.get_region("Library")
library_location = self.random.choice(
[loc for loc in library_region.locations if not loc.name.startswith("Storage: ")]
)
#Place library escape items. Choose a location to place the escape item
library_region = self.multiworld.get_region("Library", self.player)
librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")])
#Roll for which escape items will be placed in the Library
# Roll for which escape items will be placed in the Library
library_random = self.random.randint(1, 3)
if library_random == 1:
librarylocation.place_locked_item(self.create_item("Crawling"))
if library_random == 1:
library_location.place_locked_item(self.create_item("Crawling"))
item_pool = [item for item in item_pool if item.name != "Crawling"]
elif library_random == 2:
library_location.place_locked_item(self.create_item("Key for Library"))
item_pool = [item for item in item_pool if item.name != "Key for Library"]
elif library_random == 3:
library_location.place_locked_item(self.create_item("Key for Three Floor Elevator"))
library_location_2 = self.random.choice(
[loc for loc in library_region.locations if
not loc.name.startswith("Storage: ") and loc != library_location]
)
library_location_2.place_locked_item(self.create_item("Key for Egypt Room"))
item_pool = [item for item in item_pool if
item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]]
itempool = [item for item in itempool if item.name != "Crawling"]
elif library_random == 2:
librarylocation.place_locked_item(self.create_item("Key for Library Room"))
itempool = [item for item in itempool if item.name != "Key for Library Room"]
elif library_random == 3:
librarylocation.place_locked_item(self.create_item("Key for Three Floor Elevator"))
librarylocationkeytwo = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:") and loc != librarylocation])
librarylocationkeytwo.place_locked_item(self.create_item("Key for Egypt Room"))
itempool = [item for item in itempool if item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]]
#If front door option is on, determine which set of keys will be used for lobby access and add front door key to item pool
lobby_access_keys = 1
# If front door option is on, determine which set of keys will
# be used for lobby access and add front door key to item pool
lobby_access_keys = 0
if self.options.front_door_usable:
lobby_access_keys = self.random.randint(1, 2)
itempool += [self.create_item("Key for Front Door")]
lobby_access_keys = self.random.randint(0, 1)
item_pool.append(self.create_item("Key for Front Door"))
else:
itempool += [self.create_item("Heal")]
item_pool.append(self.create_item("Heal"))
self.multiworld.itempool += itempool
def set_lobby_access_keys(items: Dict[str, int]):
if lobby_access_keys == 0:
items["Key for Underground Lake"] = 1
items["Key for Office Elevator"] = 1
items["Key for Office"] = 1
else:
items["Key for Front Door"] = 1
#Lobby acess:
# Lobby access:
if self.options.lobby_access == "early":
if lobby_access_keys == 1:
self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1
self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1
self.multiworld.early_items[self.player]["Key for Office"] = 1
elif lobby_access_keys == 2:
self.multiworld.early_items[self.player]["Key for Front Door"] = 1
if self.options.lobby_access == "local":
if lobby_access_keys == 1:
self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1
self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1
self.multiworld.local_early_items[self.player]["Key for Office"] = 1
elif lobby_access_keys == 2:
self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1
set_lobby_access_keys(self.multiworld.early_items[self.player])
elif self.options.lobby_access == "local":
set_lobby_access_keys(self.multiworld.local_early_items[self.player])
#Pot piece shuffle location:
if self.options.location_pot_pieces == "own_world":
self.options.local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"}
if self.options.location_pot_pieces == "different_world":
self.options.non_local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"}
goal_item_code = SHIVERS_ITEM_ID_OFFSET + 100 + Constants.years_since_sep_30_1980
for name, data in item_table.items():
if data.type == ItemType.GOAL and data.code == goal_item_code:
goal = self.create_item(name)
self.get_location("Mystery Solved").place_locked_item(goal)
# Extra filler is random between Heals and Easier Lyre. Heals weighted 95%.
filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool) - 23
item_pool += map(self.create_item, self.random.choices(
["Heal", "Easier Lyre"], weights=[95, 5], k=filler_needed
))
self.multiworld.itempool += item_pool
def pre_fill(self) -> None:
# Prefills event storage locations with duplicate pots
storagelocs = []
storageitems = []
self.storage_placements = []
storage_locs = []
storage_items = []
for locations in Constants.location_info["locations_by_region"].values():
for loc_name in locations:
if loc_name.startswith("Accessible: "):
storagelocs.append(self.multiworld.get_location(loc_name, self.player))
if loc_name.startswith("Storage: "):
storage_locs.append(self.get_location(loc_name))
#Pot pieces/Completed/Mixed:
# Pot pieces/Completed/Mixed:
if self.options.full_pots == "pieces":
storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate']
storage_items += [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_DUPLICATE]
elif self.options.full_pots == "complete":
storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate_type2']
storageitems += [self.create_item("Empty") for i in range(10)]
storage_items += [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPLETE_DUPLICATE]
storage_items += [self.create_item("Empty") for _ in range(10)]
else:
pieces = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_DUPLICATE]
complete = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPLETE_DUPLICATE]
for i in range(10):
#Pieces
# Pieces
if self.pot_completed_list[i] == 0:
storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i])]
storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])]
#Complete
storage_items.append(pieces[i])
storage_items.append(pieces[i + 10])
# Complete
else:
storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])]
storageitems += [self.create_item("Empty")]
storage_items.append(complete[i])
storage_items.append(self.create_item("Empty"))
storageitems += [self.create_item("Empty") for i in range(3)]
storage_items += [self.create_item("Empty") for _ in range(3)]
state = self.multiworld.get_all_state(True)
self.random.shuffle(storagelocs)
self.random.shuffle(storageitems)
fill_restrictive(self.multiworld, state, storagelocs.copy(), storageitems, True, True)
self.random.shuffle(storage_locs)
self.random.shuffle(storage_items)
self.storage_placements = {location.name: location.item.name for location in storagelocs}
fill_restrictive(self.multiworld, state, storage_locs.copy(), storage_items, True, True)
set_rules = set_rules
self.storage_placements = {location.name.replace("Storage: ", ""): location.item.name.replace(" DUPE", "") for
location in storage_locs}
def fill_slot_data(self) -> dict:
return {
"StoragePlacements": self.storage_placements,
"ExcludedLocations": list(self.options.exclude_locations.value),

View File

@@ -11,7 +11,7 @@
"Information Plaque: (Ocean) Poseidon",
"Information Plaque: (Ocean) Colossus of Rhodes",
"Information Plaque: (Ocean) Poseidon's Temple",
"Information Plaque: (Underground Maze) Subterranean World",
"Information Plaque: (Underground Maze Staircase) Subterranean World",
"Information Plaque: (Underground Maze) Dero",
"Information Plaque: (Egypt) Tomb of the Ixupi",
"Information Plaque: (Egypt) The Sphinx",

View File

@@ -19,7 +19,7 @@
"Puzzle Solved Fortune Teller Door",
"Puzzle Solved Alchemy",
"Puzzle Solved UFO Symbols",
"Puzzle Solved Anansi Musicbox",
"Puzzle Solved Anansi Music Box",
"Puzzle Solved Gallows",
"Puzzle Solved Mastermind",
"Puzzle Solved Marble Flipper",
@@ -54,7 +54,7 @@
"Final Riddle: Norse God Stone Message",
"Final Riddle: Beth's Body Page 17",
"Final Riddle: Guillotine Dropped",
"Puzzle Hint Found: Combo Lock in Mailbox",
"Puzzle Hint Found: Mailbox",
"Puzzle Hint Found: Orange Symbol",
"Puzzle Hint Found: Silver Symbol",
"Puzzle Hint Found: Green Symbol",
@@ -113,15 +113,19 @@
"Puzzle Solved Office Elevator",
"Puzzle Solved Bedroom Elevator",
"Puzzle Solved Three Floor Elevator",
"Ixupi Captured Lightning"
"Ixupi Captured Lightning",
"Puzzle Solved Combination Lock",
"Puzzle Hint Found: Beth's Note",
"Mystery Solved"
],
"locations_by_region": {
"Outside": [
"Puzzle Solved Combination Lock",
"Puzzle Solved Gears",
"Puzzle Solved Stone Henge",
"Puzzle Solved Office Elevator",
"Puzzle Solved Three Floor Elevator",
"Puzzle Hint Found: Combo Lock in Mailbox",
"Puzzle Hint Found: Mailbox",
"Puzzle Hint Found: Orange Symbol",
"Puzzle Hint Found: Silver Symbol",
"Puzzle Hint Found: Green Symbol",
@@ -130,32 +134,42 @@
"Puzzle Hint Found: Tan Symbol"
],
"Underground Lake": [
"Flashback Memory Obtained Windlenot's Ghost",
"Flashback Memory Obtained Windlenot's Ghost"
],
"Windlenot's Body": [
"Flashback Memory Obtained Egyptian Hieroglyphics Explained"
],
"Office": [
"Flashback Memory Obtained Scrapbook",
"Accessible: Storage: Desk Drawer",
"Storage: Desk Drawer",
"Puzzle Hint Found: Atlantis Map",
"Puzzle Hint Found: Tape Recorder Heard",
"Puzzle Solved Bedroom Elevator"
],
"Workshop": [
"Puzzle Solved Workshop Drawers",
"Accessible: Storage: Workshop Drawers",
"Storage: Workshop Drawers",
"Puzzle Hint Found: Basilisk Bone Fragments"
],
"Bedroom": [
"Flashback Memory Obtained Professor Windlenot's Diary"
],
"Lobby": [
"Puzzle Solved Theater Door",
"Flashback Memory Obtained Museum Brochure",
"Information Plaque: (Lobby) Jade Skull",
"Information Plaque: (Lobby) Transforming Masks",
"Storage: Slide",
"Storage: Transforming Mask"
],
"Library": [
"Puzzle Solved Library Statue",
"Flashback Memory Obtained In Search of the Unexplained",
"Flashback Memory Obtained South American Pictographs",
"Flashback Memory Obtained Mythology of the Stars",
"Flashback Memory Obtained Black Book",
"Accessible: Storage: Library Cabinet",
"Accessible: Storage: Library Statue"
"Storage: Library Cabinet",
"Storage: Library Statue"
],
"Maintenance Tunnels": [
"Flashback Memory Obtained Beth's Address Book"
@@ -163,37 +177,46 @@
"Three Floor Elevator": [
"Puzzle Hint Found: Elevator Writing"
],
"Lobby": [
"Puzzle Solved Theater Door",
"Flashback Memory Obtained Museum Brochure",
"Information Plaque: (Lobby) Jade Skull",
"Information Plaque: (Lobby) Transforming Masks",
"Accessible: Storage: Slide",
"Accessible: Storage: Transforming Mask"
],
"Generator": [
"Final Riddle: Beth's Body Page 17",
"Ixupi Captured Lightning"
],
"Theater Back Hallways": [
"Beth's Body": [
"Final Riddle: Beth's Body Page 17"
],
"Theater": [
"Storage: Theater",
"Puzzle Hint Found: Beth's Note"
],
"Theater Back Hallway": [
"Puzzle Solved Clock Tower Door"
],
"Clock Tower Staircase": [
"Clock Chains": [
"Puzzle Solved Clock Chains"
],
"Clock Tower": [
"Flashback Memory Obtained Beth's Ghost",
"Accessible: Storage: Clock Tower",
"Storage: Clock Tower",
"Puzzle Hint Found: Shaman Security Camera"
],
"Projector Room": [
"Flashback Memory Obtained Theater Movie"
],
"Prehistoric": [
"Information Plaque: (Prehistoric) Bronze Unicorn",
"Information Plaque: (Prehistoric) Griffin",
"Information Plaque: (Prehistoric) Eagles Nest",
"Information Plaque: (Prehistoric) Large Spider",
"Information Plaque: (Prehistoric) Starfish",
"Storage: Eagles Nest"
],
"Greenhouse": [
"Storage: Greenhouse"
],
"Ocean": [
"Puzzle Solved Atlantis",
"Puzzle Solved Organ",
"Flashback Memory Obtained Museum Blueprints",
"Accessible: Storage: Ocean",
"Storage: Ocean",
"Puzzle Hint Found: Sirens Song Heard",
"Information Plaque: (Ocean) Quartz Crystal",
"Information Plaque: (Ocean) Poseidon",
@@ -204,10 +227,14 @@
"Information Plaque: (Underground Maze Staircase) Subterranean World",
"Puzzle Solved Maze Door"
],
"Tar River": [
"Storage: Tar River",
"Information Plaque: (Underground Maze) Dero"
],
"Egypt": [
"Puzzle Solved Columns of RA",
"Puzzle Solved Burial Door",
"Accessible: Storage: Egypt",
"Storage: Egypt",
"Puzzle Hint Found: Egyptian Sphinx Heard",
"Information Plaque: (Egypt) Tomb of the Ixupi",
"Information Plaque: (Egypt) The Sphinx",
@@ -216,7 +243,7 @@
"Burial": [
"Puzzle Solved Chinese Solitaire",
"Flashback Memory Obtained Merrick's Notebook",
"Accessible: Storage: Chinese Solitaire",
"Storage: Chinese Solitaire",
"Information Plaque: (Burial) Norse Burial Ship",
"Information Plaque: (Burial) Paracas Burial Bundles",
"Information Plaque: (Burial) Spectacular Coffins of Ghana",
@@ -225,15 +252,14 @@
],
"Shaman": [
"Puzzle Solved Shaman Drums",
"Accessible: Storage: Shaman Hut",
"Storage: Shaman Hut",
"Information Plaque: (Shaman) Witch Doctors of the Congo",
"Information Plaque: (Shaman) Sarombe doctor of Mozambique"
],
"Gods Room": [
"Puzzle Solved Lyre",
"Puzzle Solved Red Door",
"Accessible: Storage: Lyre",
"Final Riddle: Norse God Stone Message",
"Storage: Lyre",
"Information Plaque: (Gods) Fisherman's Canoe God",
"Information Plaque: (Gods) Mayan Gods",
"Information Plaque: (Gods) Thor",
@@ -242,6 +268,9 @@
"Information Plaque: (Gods) Sumerian Lyre",
"Information Plaque: (Gods) Chuen"
],
"Norse Stone": [
"Final Riddle: Norse God Stone Message"
],
"Blue Maze": [
"Puzzle Solved Fortune Teller Door"
],
@@ -251,35 +280,46 @@
],
"Inventions": [
"Puzzle Solved Alchemy",
"Accessible: Storage: Alchemy"
"Storage: Alchemy"
],
"UFO": [
"Puzzle Solved UFO Symbols",
"Accessible: Storage: UFO",
"Final Riddle: Planets Aligned",
"Storage: UFO",
"Information Plaque: (UFO) Coincidence or Extraterrestrial Visits?",
"Information Plaque: (UFO) Planets",
"Information Plaque: (UFO) Astronomical Construction",
"Information Plaque: (UFO) Aliens"
],
"Orrery": [
"Final Riddle: Planets Aligned"
],
"Janitor Closet": [
"Storage: Janitor Closet"
],
"Werewolf": [
"Information Plaque: (Werewolf) Lycanthropy"
],
"Pegasus": [
"Information Plaque: (Pegasus) Cyclops"
],
"Anansi": [
"Puzzle Solved Anansi Musicbox",
"Puzzle Solved Anansi Music Box",
"Flashback Memory Obtained Ancient Astrology",
"Accessible: Storage: Skeleton",
"Accessible: Storage: Anansi",
"Storage: Skeleton",
"Storage: Anansi Music Box",
"Information Plaque: (Anansi) African Creation Myth",
"Information Plaque: (Anansi) Apophis the Serpent",
"Information Plaque: (Anansi) Death",
"Information Plaque: (Pegasus) Cyclops",
"Information Plaque: (Werewolf) Lycanthropy"
"Information Plaque: (Anansi) Death"
],
"Torture": [
"Puzzle Solved Gallows",
"Accessible: Storage: Gallows",
"Final Riddle: Guillotine Dropped",
"Storage: Gallows",
"Puzzle Hint Found: Gallows Information Plaque",
"Information Plaque: (Torture) Guillotine"
],
"Guillotine": [
"Final Riddle: Guillotine Dropped"
],
"Puzzle Room Mastermind": [
"Puzzle Solved Mastermind",
"Puzzle Hint Found: Mastermind Information Plaque"
@@ -287,29 +327,8 @@
"Puzzle Room Marbles": [
"Puzzle Solved Marble Flipper"
],
"Prehistoric": [
"Information Plaque: (Prehistoric) Bronze Unicorn",
"Information Plaque: (Prehistoric) Griffin",
"Information Plaque: (Prehistoric) Eagles Nest",
"Information Plaque: (Prehistoric) Large Spider",
"Information Plaque: (Prehistoric) Starfish",
"Accessible: Storage: Eagles Nest"
],
"Tar River": [
"Accessible: Storage: Tar River",
"Information Plaque: (Underground Maze) Dero"
],
"Theater": [
"Accessible: Storage: Theater"
],
"Greenhouse": [
"Accessible: Storage: Greenhouse"
],
"Janitor Closet": [
"Accessible: Storage: Janitor Closet"
],
"Skull Dial Bridge": [
"Accessible: Storage: Skull Bridge",
"Skull Bridge": [
"Storage: Skull Bridge",
"Puzzle Solved Skull Dial Door"
],
"Water Capture": [
@@ -338,6 +357,9 @@
],
"Metal Capture": [
"Ixupi Captured Metal"
],
"Victory": [
"Mystery Solved"
]
}
}

View File

@@ -4,22 +4,25 @@
["Registry", ["To Outside From Registry"]],
["Outside", ["To Underground Tunnels From Outside", "To Lobby From Outside"]],
["Underground Tunnels", ["To Underground Lake From Underground Tunnels", "To Outside From Underground"]],
["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]],
["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Windlenot's Body From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]],
["Windlenot's Body", ["To Underground Lake From Windlenot's Body"]],
["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]],
["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]],
["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office", "To Ash Capture From Office"]],
["Workshop", ["To Office From Workshop", "To Wood Capture From Workshop"]],
["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]],
["Bedroom", ["To Bedroom Elevator From Bedroom", "To Metal Capture From Bedroom"]],
["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby"]],
["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby", "To Victory"]],
["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library", "To Wax Capture From Library"]],
["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]],
["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator From Maintenance Tunnels"]],
["Generator", ["To Maintenance Tunnels From Generator"]],
["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]],
["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]],
["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]],
["Beth's Body", ["To Generator From Beth's Body"]],
["Theater", ["To Lobby From Theater", "To Theater Back Hallway From Theater"]],
["Theater Back Hallway", ["To Theater From Theater Back Hallway", "To Clock Tower Staircase From Theater Back Hallway", "To Maintenance Tunnels From Theater Back Hallway", "To Projector Room"]],
["Clock Tower Staircase", ["To Theater Back Hallway From Clock Tower Staircase", "To Clock Tower"]],
["Clock Chains", ["To Clock Tower Staircase From Clock Chains"]],
["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]],
["Projector Room", ["To Theater Back Hallways From Projector Room", "To Metal Capture From Projector Room"]],
["Projector Room", ["To Theater Back Hallway From Projector Room", "To Metal Capture From Projector Room"]],
["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric", "To Oil Capture From Prehistoric", "To Metal Capture From Prehistoric"]],
["Greenhouse", ["To Prehistoric From Greenhouse", "To Sand Capture From Greenhouse"]],
["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean", "To Crystal Capture From Ocean", "To Sand Capture From Ocean"]],
@@ -28,22 +31,26 @@
["Tar River", ["To Maze From Tar River", "To Lobby From Tar River", "To Oil Capture From Tar River"]],
["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt", "To Cloth Capture From Egypt"]],
["Burial", ["To Egypt From Burial", "To Shaman From Burial", "To Ash Capture From Burial", "To Cloth Capture From Burial"]],
["Shaman", ["To Burial From Shaman", "To Gods Room", "To Wax Capture From Shaman"]],
["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room"]],
["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi", "To Wax Capture From Anansi", "To Wood Capture From Anansi"]],
["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]],
["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]],
["Shaman", ["To Burial From Shaman", "To Gods Room From Shaman", "To Wax Capture From Shaman"]],
["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room", "To Norse Stone From Gods Room"]],
["Norse Stone", ["To Gods Room From Norse Stone"]],
["Anansi", ["To Gods Room From Anansi", "To Pegasus From Anansi", "To Wax Capture From Anansi"]],
["Pegasus", ["To Anansi From Pegasus", "To Werewolf From Pegasus", "To Wood Capture From Pegasus"]],
["Werewolf", ["To Pegasus From Werewolf", "To Night Staircase From Werewolf"]],
["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO From Night Staircase"]],
["Janitor Closet", ["To Night Staircase From Janitor Closet", "To Water Capture From Janitor Closet", "To Cloth Capture From Janitor Closet"]],
["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]],
["UFO", ["To Night Staircase From UFO", "To Orrery From UFO", "To Inventions From UFO"]],
["Orrery", ["To UFO From Orrery"]],
["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze", "To Wood Capture From Blue Maze"]],
["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]],
["Fortune Teller", ["To Blue Maze From Fortune Teller"]],
["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]],
["Torture", ["To Inventions From Torture", "To Puzzle Room Mastermind From Torture"]],
["Guillotine", ["To Torture From Guillotine"]],
["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]],
["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]],
["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]],
["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]],
["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Bridge From Puzzle Room Marbles"]],
["Skull Bridge", ["To Puzzle Room Marbles From Skull Bridge", "To Slide Room"]],
["Slide Room", ["To Skull Bridge From Slide Room", "To Lobby From Slide Room"]],
["Water Capture", []],
["Wax Capture", []],
["Ash Capture", []],
@@ -52,17 +59,20 @@
["Wood Capture", []],
["Crystal Capture", []],
["Sand Capture", []],
["Metal Capture", []]
["Metal Capture", []],
["Victory", []]
],
"mandatory_connections": [
["To Registry", "Registry"],
["To Registry", "Registry"],
["To Outside From Registry", "Outside"],
["To Outside From Underground", "Outside"],
["To Outside From Lobby", "Outside"],
["To Underground Tunnels From Outside", "Underground Tunnels"],
["To Underground Tunnels From Underground Lake", "Underground Tunnels"],
["To Underground Lake From Underground Tunnels", "Underground Lake"],
["To Underground Lake From Windlenot's Body", "Underground Lake"],
["To Underground Lake From Underground Blue Tunnels", "Underground Lake"],
["To Windlenot's Body From Underground Lake", "Windlenot's Body"],
["To Underground Blue Tunnels From Underground Lake", "Underground Blue Tunnels"],
["To Underground Blue Tunnels From Office Elevator", "Underground Blue Tunnels"],
["To Office Elevator From Underground Blue Tunnels", "Office Elevator"],
@@ -86,7 +96,7 @@
["To Library From Lobby", "Library"],
["To Library From Maintenance Tunnels", "Library"],
["To Theater From Lobby", "Theater" ],
["To Theater From Theater Back Hallways", "Theater"],
["To Theater From Theater Back Hallway", "Theater"],
["To Prehistoric From Lobby", "Prehistoric"],
["To Prehistoric From Greenhouse", "Prehistoric"],
["To Prehistoric From Ocean", "Prehistoric"],
@@ -96,15 +106,17 @@
["To Maintenance Tunnels From Generator", "Maintenance Tunnels"],
["To Maintenance Tunnels From Three Floor Elevator", "Maintenance Tunnels"],
["To Maintenance Tunnels From Library", "Maintenance Tunnels"],
["To Maintenance Tunnels From Theater Back Hallways", "Maintenance Tunnels"],
["To Maintenance Tunnels From Theater Back Hallway", "Maintenance Tunnels"],
["To Three Floor Elevator From Maintenance Tunnels", "Three Floor Elevator"],
["To Three Floor Elevator From Blue Maze Bottom", "Three Floor Elevator"],
["To Three Floor Elevator From Blue Maze Top", "Three Floor Elevator"],
["To Generator", "Generator"],
["To Theater Back Hallways From Theater", "Theater Back Hallways"],
["To Theater Back Hallways From Clock Tower Staircase", "Theater Back Hallways"],
["To Theater Back Hallways From Projector Room", "Theater Back Hallways"],
["To Clock Tower Staircase From Theater Back Hallways", "Clock Tower Staircase"],
["To Generator From Maintenance Tunnels", "Generator"],
["To Generator From Beth's Body", "Generator"],
["To Theater Back Hallway From Theater", "Theater Back Hallway"],
["To Theater Back Hallway From Clock Tower Staircase", "Theater Back Hallway"],
["To Theater Back Hallway From Projector Room", "Theater Back Hallway"],
["To Clock Tower Staircase From Theater Back Hallway", "Clock Tower Staircase"],
["To Clock Tower Staircase From Clock Chains", "Clock Tower Staircase"],
["To Clock Tower Staircase From Clock Tower", "Clock Tower Staircase"],
["To Projector Room", "Projector Room"],
["To Clock Tower", "Clock Tower"],
@@ -125,30 +137,37 @@
["To Blue Maze From Egypt", "Blue Maze"],
["To Shaman From Burial", "Shaman"],
["To Shaman From Gods Room", "Shaman"],
["To Gods Room", "Gods Room" ],
["To Gods Room From Shaman", "Gods Room" ],
["To Gods Room From Norse Stone", "Gods Room" ],
["To Gods Room From Anansi", "Gods Room"],
["To Norse Stone From Gods Room", "Norse Stone" ],
["To Anansi From Gods Room", "Anansi"],
["To Anansi From Werewolf", "Anansi"],
["To Werewolf From Anansi", "Werewolf"],
["To Anansi From Pegasus", "Anansi"],
["To Pegasus From Anansi", "Pegasus"],
["To Pegasus From Werewolf", "Pegasus"],
["To Werewolf From Pegasus", "Werewolf"],
["To Werewolf From Night Staircase", "Werewolf"],
["To Night Staircase From Werewolf", "Night Staircase"],
["To Night Staircase From Janitor Closet", "Night Staircase"],
["To Night Staircase From UFO", "Night Staircase"],
["To Janitor Closet", "Janitor Closet"],
["To UFO", "UFO"],
["To UFO From Night Staircase", "UFO"],
["To UFO From Orrery", "UFO"],
["To UFO From Inventions", "UFO"],
["To Orrery From UFO", "Orrery"],
["To Inventions From UFO", "Inventions"],
["To Inventions From Blue Maze", "Inventions"],
["To Inventions From Torture", "Inventions"],
["To Fortune Teller", "Fortune Teller"],
["To Torture", "Torture"],
["To Torture From Guillotine", "Torture"],
["To Torture From Inventions", "Torture"],
["To Puzzle Room Mastermind From Torture", "Puzzle Room Mastermind"],
["To Puzzle Room Mastermind From Puzzle Room Marbles", "Puzzle Room Mastermind"],
["To Puzzle Room Marbles From Puzzle Room Mastermind", "Puzzle Room Marbles"],
["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"],
["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"],
["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"],
["To Puzzle Room Marbles From Skull Bridge", "Puzzle Room Marbles"],
["To Skull Bridge From Puzzle Room Marbles", "Skull Bridge"],
["To Skull Bridge From Slide Room", "Skull Bridge"],
["To Slide Room", "Slide Room"],
["To Wax Capture From Library", "Wax Capture"],
["To Wax Capture From Shaman", "Wax Capture"],
@@ -164,7 +183,7 @@
["To Cloth Capture From Janitor Closet", "Cloth Capture"],
["To Wood Capture From Workshop", "Wood Capture"],
["To Wood Capture From Gods Room", "Wood Capture"],
["To Wood Capture From Anansi", "Wood Capture"],
["To Wood Capture From Pegasus", "Wood Capture"],
["To Wood Capture From Blue Maze", "Wood Capture"],
["To Crystal Capture From Lobby", "Crystal Capture"],
["To Crystal Capture From Ocean", "Crystal Capture"],
@@ -172,6 +191,7 @@
["To Sand Capture From Ocean", "Sand Capture"],
["To Metal Capture From Bedroom", "Metal Capture"],
["To Metal Capture From Projector Room", "Metal Capture"],
["To Metal Capture From Prehistoric", "Metal Capture"]
["To Metal Capture From Prehistoric", "Metal Capture"],
["To Victory", "Victory"]
]
}

View File

@@ -27,5 +27,4 @@ Victory is achieved when the player has captured the required number Ixupi set i
## Encountered a bug?
Please contact GodlFire on Discord for bugs related to Shivers world generation.<br>
Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer.
Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer.

View File

@@ -7,6 +7,11 @@
- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later
- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version
## Optional Software
- [PopTracker](https://github.com/black-sliver/PopTracker/releases/)
- [Jax's Shivers PopTracker pack](https://github.com/blazik-barth/Shivers-Tracker/releases/)
## Setup ScummVM for Shivers
### GOG version of Shivers
@@ -57,4 +62,4 @@ validator page: [YAML Validation page](/mysterycheck)
- Every puzzle
- Every puzzle hint/solution
- Every document that is considered a Flashback
- Optionally information plaques.
- Optionally information plaques

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