Compare commits

...

83 Commits

Author SHA1 Message Date
Chris Wilson
f33f19f8b2 Fix options pages not redirecting to appropriate host url for /api/generate 2024-05-19 00:21:31 -04:00
Doug Hoskisson
230a9e620b Core: move OptionGroup definition to Options.py (#3325) 2024-05-19 04:40:41 +02:00
Silvris
1b6fb7b090 Tests: test that no worlds fail to load (#3318)
* test that no worlds fail to load

* pep8

* Update test_implemented.py

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-19 00:42:58 +02:00
jamesbrq
0e893889c7 MLSS: General bugfixes + Add patch extension to inno_setup.iss (#3286)
* Remove outdated header change for ROM verification

* Update Connections to be compatible with python ver. 3.8

* Update inno_setup.iss

* Update inno_setup.iss
2024-05-18 22:26:50 +02:00
Rensen3
2bc345504e YGO06: make sure it runs on 3.8 support (#3324)
* YGO06: make sure it runs on python 3.8

* YGO06: change merge of dict, so it runs on python 3.8
2024-05-18 13:53:17 +02:00
Chris Wilson
5e3c5dedf3 WebHost: Massive overhaul of options pages (#2614)
* Implement support for option groups. WebHost options pages still need to be updated.

* Remove debug output

* In-progress conversion of player-options to Jinja rendering

* Support "Randomize" button without JS, transpile SCSS to CSS, include map file for later editors

* Un-alphabetize options, add default group name for item/location Option classes, implement more option types

* Re-flow UI generation to avoid printing rows with unsupported or invalid option types, add support for TextChoice options

* Support all remaining option types

* Rendering improvements and CSS fixes for prettiness

* Wrap options in a form, update button styles, fix labels, disable inputs where the default is random, nuke the JS

* Minor CSS tweaks, as recommended by the designer

* Hide JS-required elements in noscript tag. Add JS reactivity to range, named-range, and randomize buttons.

* Fix labels, add JS handling for TextChoice

* Make option groups collapsable

* PEP8 current option_groups progress (#2604)

* Make the python more PEP8 and remove unneeded imports

* remove LocationSet from `Item & Location Options` group

* It's ugly, but YAML generation is working

* Stop generating JSON files for player-options pages

* Do not include ItemDict entries whose values are zero

* Properly format yaml output

* Save options when form is submitted, load options on page load

* Fix options being omitted from the page if a group has an even number of options

* Implement generate-game, escape option descriptions

* Fix "randomize" checkboxes not properly setting YAML options to "random"

* Add a separator between item/location groups and items/locations in their respective lists

* Implement option presets

* Fix docs to detail what actually ended up happening

* implement option groups on webworld to allow dev sorting (#2616)

* Force extremely long item/location/option names with no spaces to text-wrap

* Fix "randomize" button being too wide in single-column display, change page header to include game name

* Update preset select to read "custom" when updating form inputs. Show error message if the user doesn't input a name

* Un-break weighted-options, add option group names to weighted options

* Nuke weighted-options. Set up framework to rebuild it in Jinja.

* Generate styles with scss, remove styles which will be replaced, add placeholders for worlds

* Support Toggle, DefaultOnToggle, and Choice options in weighted-options

* Implement expand/collapse without JS for worlds and option groups

* Properly style set options

* Implement Range and NamedRange. Also, CSS is hard.

* Add support for remaining option types. JS and backend still forthcoming.

* Add JS functionality for collapsing game divs, populating span values on range updates. Add <noscript> tag to warn users with JS disabled.

* Support showing/hiding game divs based on range value for game

* Add support for adding/deleting range rows

* Save settings to localStorage on form submission

* Save deleted options on form submission

* Break weighted-options into a per-game page.

- Break weighted-options into a per-game page
- Add "advanced options" links to supported games page
- Use details/summary tags on supported games, player-options, and weighted-options
- Fix bug preventing previously deleted rows from being removed on page load if JS is enabled
- Move route handling for options pages to options.py
- Remove world handling from weighted-options

* Implement loading previous settings from localStorage on page load if JS is enabled

* Weighted options can now generate YAML files and single-player games

* options pages now respect option visibility settings for simple and complex pages

* Remove `/weighted-settings` redirect, fix weighted-options link on player-options page

* Fix instance of AutoWorld not having access to proper `random`

* Catch instances of frozenset along with set

* Restore word-wrap in tooltips

* Fix word wrap in player-options labels

* Add `dedent` filter to help with formatting tooltips in player-options

* Do not change the ordering of keys when printing yaml files

* Move necessary import out of conditional statement

* Expand only the first option group by default on both options pages

* Respect option visibility when generating yaml template files

* Swap to double quotes

* Replace instances of `/weighted-settings` with `/weighted-options`, swap out incomplete links

* Strip newlines and spaces after applying dedent filter

* Fix documentation for option groups

* Update site map

* Update various docs

* Sort OptionSet lists alphabetically

* Minor style tweak

* Fix extremely long text overflowing tooltips

* Convert player-options to use CSS grid instead of tables

* Do not display link to weighted-options page on supported games if the options page is an external link

* Update worlds/AutoWorld.py

Bugfix by @alwaysintreble

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

* Fix NamedRange options not being properly set if a preset it loaded

* Move option-presets route into options.py

* Include preset name in YAML if not "default" and not "custom"

* Removed macros for PlandoBosses and DefaultOnToggle, as they were handled by their parent classes

* Fix not disabling custom inputs when the randomize button is clicked

* Only sort OptionList and OptionSet valid_keys if they are unordered

* Quick style fixes for player-settings to give `select` elements `text-overflow: ellipsis` and increase base size of left-column

* Prevent showing a horizontal scroll bar on player-options if the browser width was beneath a certain threshold

* Fix a bug in weighted-options which prevented inputting a negative value for new range inputs

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-05-18 00:11:57 -04:00
NewSoupVi
5fb0126754 Core: Player name property on world class (#3042)
* player property on world class

* Remove dat shi from overcooked

* Update worlds/AutoWorld.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-05-18 00:18:57 +02:00
Rensen3
b4c263fc9d YGO06: add new game yugioh06 to CODEOWNERS inno_setup and readme (#3316) 2024-05-18 00:09:03 +02:00
Bryce Wilson
013862b068 Pokemon Emerald: Update changelog (#3317)
* Pokemon Emerald: Update changelog

* Pokemon Emerald: Fix spelling error in changelog

Co-authored-by: Remy Jette <remy@remyjette.com>

---------

Co-authored-by: Remy Jette <remy@remyjette.com>
2024-05-18 00:06:30 +02:00
Doug Hoskisson
280b67f996 some worlds: some typing in LocalRom (#3090)
* some worlds: some typing in `LocalRom`

### `read_bytes`

It's not safe to return `bytearray` when we think it's `bytes`
```python
a = rom.read_bytes(8, 3)
hash(a)  # This won't crash, right?
```

### `write_bytes`

`Iterable[SupportsIndex]` is what's required for `bytearray.__setitem__(slice, values)`
We need to add `__len__` for the `len(values)` in this function.

* remove `object` inheritance
2024-05-17 21:41:57 +02:00
NewSoupVi
9ae7083bfc Fix Monastery Entry RIght righqeuotghqeougtfgas (#3213) 2024-05-17 19:29:55 +02:00
NewSoupVi
bd18018852 The Witness: Fix Mountain Floor 2 Near Row 5 Symbol Requirement (#3212) 2024-05-17 19:29:46 +02:00
Exempt-Medic
b4b79bcd78 BRCF: Small Fixes (#3314)
* Plural fix

* Update link
2024-05-17 19:24:32 +02:00
Rensen3
539ee1c5da Yu-Gi-oh! 2006: implement new game (#2795)
* Initial implementation of Yu-Gi-Oh! WC 2006

* Added Opponents and banlists

* Initial implementation of Yu-Gi-Oh! WC 2006

* Added Opponents and banlists

* Added Campaign Logic

* Added Bonuses Logic

* Added challenge logic

* fixed yugioh client

* ygo06 rom cleanup and include lua

* ygo06 patch cleanup

* ygo06 move client to world folder

* lots of small changes

* bug fixes

* implemented filler item for yugioh06

* BizHawkClient: Add client and connector

* BizHawkClient: Add launcher component and inno_setup lines

* BizHawkClient: Misc stability updates and small improvements

Bad commit organization a consequence of working with two different branches and not keeping the commits separated

* BizHawkClient: Add docstrings

* BizHawkClient: Pull in changes from other branch

* BizHawkClient: Fix no handler message not displaying after changed ROMs

* BizHawkClient: Remove extra print statement from lua

* BizHawkClient: Change version command to use raw strings

* BizHawkClient: Change script version to single integer

* YGO06: added logic for "all expect type forbidden" limited duels

* YGO06: Structure Deck choice now affects logic. Fixed a bug with tier 5 campaign opponents. Added logic for TD16 Union.

* BizHawkClient: Add newline to version for lua script

* BizHawkClient: Call send_connect from BizHawkClient's watcher loop

* BizHawkClient: Add handling for failed request getting script version

* BizHawkClient: Have base64.lua check lua version explicitly for bit operations

On 2.9, it would detect LuaJIT and flood the console with deprecation warnings

* BizHawkClient: Update connector script for slightly better errors and address Gambatte frame sync issue

* BizHawkClient: Remove accidentally added print statements

* BizHawkClient: Fix connector server not closing correctly

* BizHawkClient: Move some connector code around, some linting

* BizHawkClient: Small cleanup in lua

* BizHawkClient: Lua linting

* BizHawkClient: Remove outdated sentences in docstrings

* YGO06: Logic additions and bug fixes

* BizHawkClient: Correctly null check patch file arg

* BizHawkClient: Initialize logging

* BizHawkClient: Move code to worlds/_bizhawk

Also splits out BizHawk communication functions to their own file for use outside this client

* BizHawkClient: Add license to connector lua, add types to docs

* BizHawkClient: Add module docstrings

* YGO06: Logic additions

* BizHawkClient: Allow clients to define multiple systems

* BizHawkClient: Better logging and handling of interruptions to connection to script

* YGO06: Logic additions

* YGO06: Added text to options

* YGO06: Ported to bizhawk client

* YGO06: fix goal not being detected

* YGO06: fix access item rule for tier 5 column 1 and 2

* YGO06: docu and bug fixes

* YGO06: change name

* YGO06: some fixes

* YGO06: fix starting opponent and booster not applying

* YGO06: added option to reduce the amount of challenges and remove the no ban list from pool.

* YGO06: added rom being asked for on first use

* YGO06: fix rules for challenges

* YGO06: create proper rules for TD04 Ritual Summon

* YGO06: mark most banlists as usefull instead of progression

* YGO06: reduce the required core boosters across the board

* YGO06: fix client not loading if another game already loaded the bizhawk client

* YGO06: fix client not finding the bizhawk client.

* YGO06: fix TD08 Draw not giving out an item

* YGO06: small text changes

* YGO06: update to version 0.4.4

* YGO06: logic mixin clean-up

* YGO06: added option for campaign opponents as goal

* Pokemon Emerald add encounter table randomization

* Pokemon Emerald: Item ball randomization working

* Pokemon Emerald: Clean up code a little

* Pokemon Emerald: Partial rework of region/location creation

* Pokemon Emerald: Dedupe items and add more readable names

* Refactor region creation to manually defined regions

* Split region json

* Use new data.json with flattened constants and add HM locations

* YGO06: bug fixes

* YGO06: bug fix

* YGO06: changes default options to be more beginner friendly

* YGO06: attempt at universal tracker support. Settings are stored in slot data now.

* YGO06: fix for older python versions

* YGO06: fix slot data

* YGO06: added diiferent opponents to the campaign

* YGO06: fix small bug with opponent icons

* YGO06: fix unwanted changes

* YGO06: repair merge with main

* YGO06: map out all of the opponents

* YGO06: added opponent shuffle

* YGO06: added logic to opponent shuffle

* YGO06: added option to use ocg art

* YGO06: bug_fixes

* YGO06: removed todos, since they are not needed anymore

* YGO06: added draft mode

* YGO06: added logic to draft mode

* YGO06: Added Money multiplier when you lose

* YGO06: Fixed Unit Test errors

* YGO06: Added Random deck option

* YGO06: Bug fix with registering client

* YGO06: client clean-up

* YGO06: fixed card misspellings

* YGO06: removed unused imports and other small changes

* YGO06: small changes

* YGO06: fix generation error when the combination of starting with "No Banlist" and not adding "No Banlist" to the pool is selected

* YGO06: fix ocg art path overwriting Huge Revolution bugfix

* YGO06: added comments and other minor changes

* YGO06: fixed byte length in client for money

* YGO06: fixes for webhost and options

* YGO06: use the proper random function

* YGO06: change settings to options

* YGO06: move to procedure patch

* YGO06: fix imports

* YGO06: fix download link for patch not showing

* YGO06: remove unnecessary Optional

* YGO06: fix universal tracker stuff

* YGO06: add typings

* YGO06: small cleanup

* yugioh06:  small change to setup

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

* YGO06: remove logic mixin

* YGO06: fix create item and implement create filler and get filler item name

* YGO06: remove double lambdas

* YGO06: use pkgutil.get_data instaed pf zipFile

* YGO06: fix starting items being duplicated

* YGO06: lots of small changes

* YGO06: moved functions to match execution order

* YGO06: run ruff

* YGO06: run ruff format

* YGO06: fix ruff errors

* YGO06: undo ruff format for rules

* YGO06: move import to prevent circular dependency

* YGO06: remove unused class

* YGO06: optimizing rules

* YGO06: some optimization and small bug fix

---------

Co-authored-by: Zunawe <gyroscope15@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-05-17 19:23:05 +02:00
Exempt-Medic
5fb1d0f98a FF1: Switching Options System (#3302) 2024-05-17 19:19:55 +02:00
Louis M
89a2a3c35b Aquaria: implement new game (#3197)
This is a new world for the Aquaria game (https://www.bit-blot.com/aquaria/).
2024-05-17 12:29:00 +02:00
Fabian Dill
7900e4c9a4 WebHost: use a limited process pool to run Rooms (#3214) 2024-05-17 12:21:01 +02:00
Fabian Dill
3dbdd048cd Core: prevent "Could not find identify Component responsible for None" from being logged. (#3225) 2024-05-17 12:19:41 +02:00
Trevor L
68323b46a9 Bomb Rush Cyberfunk: Implement new game (#2925)
Adds Team Reptile's Bomb Rush Cyberfunk as a new game.
2024-05-17 12:13:40 +02:00
Aaron Wagener
2447be92d8 The Messenger: fix generation failure for no portal shuffle with 3 available portals (#3200) 2024-05-17 10:18:50 +02:00
NewSoupVi
88dd27eb3a The Witness: Use OptionError (#3258)
* Use OptionError

* ruff
2024-05-17 10:07:38 +02:00
Exempt-Medic
6d8ac5d054 Core: Remove deprecated get_current_option_name and SpecialRange (#3296)
* Removing deprecated function

* Removing SpecialRange
2024-05-17 10:02:25 +02:00
Exempt-Medic
5a2d839412 Removing deprecated item_count (#3309) 2024-05-17 09:54:57 +02:00
Doug Hoskisson
4bd4a2c541 Docs: remove obsolete yaml generation info (#3304)
* Docs: remove obsolete yaml generation info

This line was added when we didn't have the "Generate Template Options" button in the launcher.

* add information about `Launcher.py`
2024-05-17 01:26:43 +02:00
FlySniper
705cb2e816 Wargroove: Switched to options API. (#3306)
* Wargroove: Switched to options API.

* Update Options.py

* Update __init__.py

* Options is plural

* Wargroove: Options updates with some small fixes.

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-16 18:46:13 +02:00
NewSoupVi
467bbd7754 WebHost: Fix setup guide link not working for games with special characters (#3269)
* WebHost: Fix setup guide link not working for games with special characters

* use url_for with _anchor (#3279)
2024-05-15 21:40:40 -04:00
LiquidCat64
4da9cdd91c CV64: Fix items with weird characters landing on Renon's shop crashing (#3305) 2024-05-15 23:50:04 +02:00
chandler05
6576b069f2 Hylics 2: Remove Random Start option and replace it with Start Location option (#3289)
* Hylics 2: Remove Random Start option and replace it with Start Location option

* remove choice

* Readd random start to slot data

* newlines

* Add random_start as a Removed option
2024-05-14 20:35:32 +02:00
Scipio Wright
b78781ab3e Docs: Update advanced yaml guide wording for priority locations (#3298)
* Update advanced yaml guide wording

* Update options api as well

* Update exclude locations description slightly to use more current verbiage

* Update priority locations in options api.md to note what happens if it runs out

* Remove auto-added bullet points

* Slightly mess with wording to make it more succinct
2024-05-14 20:28:15 +02:00
Aaron Wagener
9a82edc931 World: remove ClassVar typing from topology_present (#3294) 2024-05-14 04:35:33 +02:00
Doug Hoskisson
77cce68c08 Zillion: remove deprecated Logger.warn (#3295) 2024-05-13 20:31:15 +02:00
Exempt-Medic
f38655d6b6 Bumper Stickers and Meritous: Options and world: multiworld fixes (#3281)
* Update Options.py

* Update __init__.py

* Correct case

* Correct case

* Update Meritous and actually use Options

* Oops

* Fixing world: multiworld
2024-05-12 18:52:34 +02:00
Exempt-Medic
701fbab837 Core: World: MultiWorld and another deprecated option getter (#3254)
* world: multiworld and deprecated options getting

* Oops

* Found two more
2024-05-12 18:51:20 +02:00
Aaron Wagener
af83050b75 Core: log warning for unknown options (#1385)
* throw an error for unknown options

* move the error to the end of trigger resolution and make trigger names valid

* add bad hardcoded stuff for LTTP

* use itertools.chain instead of a ChainMap

* remove accidental unused import

* make the check after both trigger resolutions so no valid keys are missed, and only check relevant game.

* log a warning instead of crashing

* delete options from the weights once it gets registered for cleaner erroring

* grammar hard

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-05-10 23:00:13 +02:00
Exempt-Medic
8db3e40094 Removing old option getters (#3285) 2024-05-10 16:29:07 +02:00
Trevor L
d48f2ab1b4 Core: Add list/item group exclusive methods to CollectionState (#3192)
* Group exclusive methods

* Add docstrings

* Apply suggestions from code review

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

* Put lines back with no whitespace

* Add list methods

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-08 18:34:32 +02:00
Bryce Wilson
0f1b16d640 Pokemon Emerald: Change Lilycove access logic (#3277)
* Pokemon Emerald: Change logical access to lilycove from east

* Pokemon Emerald: Add tests
2024-05-08 18:26:13 +02:00
Bryce Wilson
76962b8b3b Pokemon Emerald: Fix incorrect access to slateport water encounters (#3243) 2024-05-07 12:43:35 +02:00
NewSoupVi
e04db57dce Core: Add has_list and count_list and improve (?) count_group (#2934)
* Add has_list and count_list and improve (?) count_group

* MESSENGER STOP

* Add docstrings to has_list and count_list

* Add docstrings for has_group and count_group as well

* oops

* Rename to has_from

* docstrings improvement again

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

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

* Update BaseClasses.py

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

* Update BaseClasses.py

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

* Mapping instead of Dict

---------

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

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

* Commit for PR

* Update worlds/mlss/Client.py

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

* Update worlds/mlss/__init__.py

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

* Update worlds/mlss/__init__.py

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

* Update worlds/mlss/docs/setup_en.md

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

* Remove deprecated import. Updated settings and romfile syntax

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

* Changed switch statements to if else

* Update en_Mario & Luigi Superstar Saga.md

* Updated client.py

* Update Client.py

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

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

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

* Update __init__.py

* Changed reference from world to mlssworld

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

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

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

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

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

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

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

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

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

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

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

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

* Fix merge conflict + update prep

* v1.2

* Leftover print commands

* Update basepatch.bsdiff

* Update basepatch.bsdiff

* v1.3

* Update Rom.py

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

* Event removal continuation.

* Partial Implementation of APPP (Incomplete))

* v1.4 Implemented APPP

* Docs Updated

* Update Rom.py

* Update setup_en.md

* Update Rom.py

* Update Rules.py

* Fix for APPP being broken on webhost

* Update Rom.py

* Update Rom.py

* Location name fixes + pants color fixes

* Update Rules.py

* Fix for ultra hammer cutscene

* Fixed compat. issues with python ver. 3.8

* Updated hidden block yaml option

* pre-v1.5

* Update Client.py

* Update basepatch.bsdiff

* v1.5

* Update XP multiplier to have a minimum of 0

* Update 'Beanfruit' to 'Bean Fruit'

* v1.6

* Update Rom.py

* Update basepatch.bsdiff

* Initial review refactor

* Revert state logic changes. Continuation of refactor.

* Fixed failed generations. Finished refactor.

* Reworked colors. Removed all .txt files

* Actually removed the .txt files this time

* Update Rom.py

* Update README.md

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

* Update worlds/mlss/Options.py

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

* Update worlds/mlss/Client.py

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

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

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

* Update worlds/mlss/__init__.py

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

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

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

* Update worlds/mlss/Data.py

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

* Review refactor.

* Update README.md

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

* Update worlds/mlss/Rules.py

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

* Add coin blocks to LocationName

* Refactor.

* Update Items.py

* Delete mlss.apworld

* Small asm bugfix

* Update basepatch.bsdiff

* Client sends less messages to server

* Update basepatch.bsdiff

---------

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

* Pokemon Emerald: Add regi doors event flag

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

* Handle new locations

* Support higher Strawberry counts

* Don't add start inventory items to the pool

* Support Move Shuffle functionality and items

* Hard and Move Shuffle Logic

* Fix Options

* Update CHANGELOG.md

* Add standard moves logic for signs 3 and 4

* Fix Option Tooltip

* Add tracker link to setup guide

* Fix unit test

* Fix option tooltips

* Missing Space

* Move option checking out of rule function

* Delete just_gen500.bat
2024-05-05 08:58:49 +02:00
Bryce Wilson
7e61211365 Pokemon Emerald: Convert to procedure patch (#2995)
* Pokemon Emerald: Convert to procedure patch

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

* Pokemon Emerald: Add APPP implication to changelog

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

* Pokemon Emerald: Modify changelog versions

* Pokemon Emerald: Fix patch file download not appearing

---------

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

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

* oop

* pass locations to sweep_for_events instead of the player

* finally found the diff that was breaking swap

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

---------

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

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

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

* Change how old_hints is modified/done

---------

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

* Starting off, setting up some names

* It lives

* Some preliminary plando connection handling, probably has errors

* Add missed comma

* if -> elif

* I think this is working properly to handle plando connections

* Update comments

* Fix up shop -> shop portal stuff

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

* Remove unnecessary if else

* add back the actually necessary if but not the else

* okay they were both necessary

* Update entrance rando description

* blasphemy

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

* Rename other instances of tunc -> tunic

* Update per Vi's review (thank you)

* Fix a not that shouldn't have been

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

* Fix indent

* Add a .value

* Add .values

* Fix bad comparison

* Add a not that was supposed to be there

* Replace another isinstance

* Revise option description

* Fix per Kaito's comment

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

---------

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

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

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

* Update Generate.py

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

---------

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

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

* Capitalize existing location groups

* Capitalize new boss location group names

* Update comment with capitalization

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

* Same change but for Spanish

* Update setup_en.md

* catching the bottom link as well

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

* scope creep pog

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

* apply berserker's suggestion

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

* Remove extra location group for shops

* Fire rod for magic wand

* Capitalize itme name groups

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

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

* Fix rule with item group name

* Capitalization is cool

* Fix merge mistake

* Add Flask group, remove Potions group

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

* Revise per Vi's comment

* Fix test

* fuzzy matching please stop

* Remove test change that was meant for a different branch

---------

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

* fix some formatting

---------

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

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

* data in one place instead of 3

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

---------

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

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

---------

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

* Fixed absolute imports in unit test

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

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

* Fix Triforce Hunt HUD always present

* Fix Circle of Pots enemy byte

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

View File

@@ -11,8 +11,8 @@ from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar
import NetUtils
import Options
@@ -707,15 +707,49 @@ class CollectionState():
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
def item_count(self, item: str, player: int) -> int:
Utils.deprecate("Use count instead.")
return self.count(item, player)
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name]
if found >= count:
return True
return False
def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)
def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
@@ -724,12 +758,34 @@ class CollectionState():
return True
return False
def count_group(self, item_name_group: str, player: int) -> int:
def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
found += player_prog_items[item_name]
return found
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False
def count_group(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)
def count_group_exclusive(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state.
Ignores duplicates of the same item."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name] > 0
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)
# Item related
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
@@ -990,7 +1046,7 @@ class Location:
self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))))
@@ -1186,7 +1242,7 @@ class Spoiler:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([multiworld.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:

View File

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

22
Fill.py
View File

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

View File

@@ -120,7 +120,7 @@ def main(args=None, callback=ERmain):
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
for filename, yaml_data in weights_cache.items():
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
@@ -353,7 +353,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
raise Exception(f"Error generating meta option {option_key} for {game}.")
raise Options.OptionError(f"Error generating meta option {option_key} for {game}.")
def roll_linked_options(weights: dict) -> dict:
@@ -378,7 +378,7 @@ def roll_linked_options(weights: dict) -> dict:
return weights
def roll_triggers(weights: dict, triggers: list) -> dict:
def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers):
@@ -401,7 +401,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
valid_keys.add(key)
except Exception as e:
raise ValueError(f"Your trigger number {i + 1} is invalid. "
f"Please fix your triggers.") from e
@@ -409,27 +409,29 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
try:
if option_key in game_weights:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
del game_weights[option_key]
else:
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
player_option = option.from_any(option.default) # call the from_any here to support default "random"
setattr(ret, option_key, player_option)
except Exception as e:
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
else:
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
if "linked_options" in weights:
weights = roll_linked_options(weights)
valid_trigger_names = set()
if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"])
weights = roll_triggers(weights, weights["triggers"], valid_trigger_names)
requirements = weights.get("requires", {})
if requirements:
@@ -469,7 +471,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names)
game_weights = weights[ret.game]
ret.name = get_choice('name', weights)
@@ -478,6 +480,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key, option in world_type.options_dataclass.type_hints.items():
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key in game_weights:
if option_key in {"triggers", *valid_trigger_names}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "A Link to the Past":

View File

@@ -259,7 +259,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif not args:
args = {}
if "Patch|Game|Component" in args:
if args.get("Patch|Game|Component", None) is not None:
file, component = identify(args["Patch|Game|Component"])
if file:
args['file'] = file

View File

@@ -175,11 +175,13 @@ class Context:
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.Set[str]]
logger: logging.Logger
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False):
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
self.logger = logger
super(Context, self).__init__()
self.slot_info = {}
self.log_network = log_network
@@ -287,12 +289,12 @@ class Context:
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
self.logger.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
return False
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
self.logger.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
@@ -301,12 +303,12 @@ class Context:
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
self.logger.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
return False
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
self.logger.info(f"Outgoing message: {msg}")
return True
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
@@ -317,11 +319,11 @@ class Context:
try:
websockets.broadcast(sockets, msg)
except RuntimeError:
logging.exception("Exception during broadcast_send_encoded_msgs")
self.logger.exception("Exception during broadcast_send_encoded_msgs")
return False
else:
if self.log_network:
logging.info(f"Outgoing broadcast: {msg}")
self.logger.info(f"Outgoing broadcast: {msg}")
return True
def broadcast_all(self, msgs: typing.List[dict]):
@@ -330,7 +332,7 @@ class Context:
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
logging.info("Notice (all): %s" % text)
self.logger.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]):
@@ -352,7 +354,7 @@ class Context:
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
if not client.auth:
return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
@@ -451,7 +453,7 @@ class Context:
for game_name, data in decoded_obj.get("datapackage", {}).items():
if game_name in game_data_packages:
data = game_data_packages[game_name]
logging.info(f"Loading embedded data package for game {game_name}")
self.logger.info(f"Loading embedded data package for game {game_name}")
self.gamespackage[game_name] = data
self.item_name_groups[game_name] = data["item_name_groups"]
if "location_name_groups" in data:
@@ -483,7 +485,7 @@ class Context:
with open(self.save_filename, "wb") as f:
f.write(zlib.compress(encoded_save))
except Exception as e:
logging.exception(e)
self.logger.exception(e)
return False
else:
return True
@@ -501,9 +503,9 @@ class Context:
save_data = restricted_loads(zlib.decompress(f.read()))
self.set_save(save_data)
except FileNotFoundError:
logging.error('No save data found, starting a new game')
self.logger.error('No save data found, starting a new game')
except Exception as e:
logging.exception(e)
self.logger.exception(e)
self._start_async_saving()
def _start_async_saving(self):
@@ -520,11 +522,11 @@ class Context:
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
time.sleep(max(1.0, next_wakeup))
if self.save_dirty:
logging.debug("Saving via thread.")
self.logger.debug("Saving via thread.")
self._save()
except OperationalError as e:
logging.exception(e)
logging.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
self.logger.exception(e)
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
else:
self.save_dirty = False
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
@@ -598,7 +600,7 @@ class Context:
if "stored_data" in savedata:
self.stored_data = savedata["stored_data"]
# count items and slots from lists for items_handling = remote
logging.info(
self.logger.info(
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
f'for {sum(k[2] for k in self.received_items)} players')
@@ -640,13 +642,13 @@ class Context:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
self.logger.exception(e)
self.logger.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
self.logger.debug(f"Unrecognized server option {key}")
def get_aliased_name(self, team: int, slot: int):
if (team, slot) in self.name_aliases:
@@ -680,7 +682,7 @@ class Context:
self.hints[team, player].add(hint)
new_hint_events.add(player)
logging.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
for slot in new_hint_events:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
@@ -688,7 +690,7 @@ class Context:
clients = self.clients[team].get(slot)
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))
@@ -739,21 +741,21 @@ async def server(websocket, path: str = "/", ctx: Context = None):
try:
if ctx.log_network:
logging.info("Incoming connection")
ctx.logger.info("Incoming connection")
await on_client_connected(ctx, client)
if ctx.log_network:
logging.info("Sent Room Info")
ctx.logger.info("Sent Room Info")
async for data in websocket:
if ctx.log_network:
logging.info(f"Incoming message: {data}")
ctx.logger.info(f"Incoming message: {data}")
for msg in decode(data):
await process_client_cmd(ctx, client, msg)
except Exception as e:
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
ctx.logger.exception(e)
finally:
if ctx.log_network:
logging.info("Disconnected")
ctx.logger.info("Disconnected")
await ctx.disconnect(client)
@@ -803,14 +805,25 @@ async def on_client_disconnected(ctx: Context, client: Client):
await on_client_left(ctx, client)
_non_game_messages = {"HintGame": "hinting", "Tracker": "tracking", "TextOnly": "viewing"}
""" { tag: ui_message } """
async def on_client_joined(ctx: Context, client: Client):
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version)
verb = "tracking" if "Tracker" in client.tags else "playing"
for tag, verb in _non_game_messages.items():
if tag in client.tags:
final_verb = verb
break
else:
final_verb = "playing"
ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"{verb} {ctx.games[client.slot]} has joined. "
f"{final_verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}.",
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
ctx.notify_client(client, "Now that you are connected, "
@@ -825,8 +838,19 @@ async def on_client_left(ctx: Context, client: Client):
if len(ctx.clients[client.team][client.slot]) < 1:
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
version_str = '.'.join(str(x) for x in client.version)
for tag, verb in _non_game_messages.items():
if tag in client.tags:
final_verb = f"stopped {verb}"
break
else:
final_verb = "left"
ctx.broadcast_text_all(
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has {final_verb} the game. "
f"Client({version_str}), {client.tags}.",
{"type": "Part", "team": client.team, "slot": client.slot})
@@ -963,7 +987,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
ctx.player_names[(team, target_player)], ctx.location_names[location]))
info_text = json_format_send_event(new_item, target_player)
@@ -1507,15 +1531,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
self.ctx.notify_hints(self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
old_hints = list(set(hints) - new_hints)
if old_hints and not new_hints:
self.ctx.notify_hints(self.client.team, old_hints)
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]
if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
@@ -1527,7 +1549,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
hints = found_hints + old_hints
while can_pay > 0:
if not not_found_hints:
break
@@ -1537,6 +1559,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
self.ctx.notify_hints(self.client.team, hints)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
@@ -1550,7 +1573,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
self.ctx.notify_hints(self.client.team, hints)
self.ctx.save()
return True
@@ -1605,7 +1627,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
try:
cmd: str = args["cmd"]
except:
logging.exception(f"Could not get command from {args}")
ctx.logger.exception(f"Could not get command from {args}")
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
"text": f"Could not get command from {args} at `cmd`"}])
raise
@@ -1631,7 +1653,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
team, slot = ctx.connect_names[args['name']]
game = ctx.games[slot]
ignore_game = ("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game")
ignore_game = not args.get("game") and any(tag in _non_game_messages for tag in args["tags"])
if not ignore_game and args['game'] != game:
errors.add('InvalidGame')
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
@@ -1646,7 +1670,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if ctx.compatibility == 0 and args['version'] != version_tuple:
errors.add('IncompatibleVersion')
if errors:
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
ctx.logger.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
else:
team, slot = ctx.connect_names[args['name']]
@@ -2198,7 +2222,7 @@ async def console(ctx: Context):
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
defaults = Utils.get_options()["server_options"].as_dict()
defaults = Utils.get_settings()["server_options"].as_dict()
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int)
@@ -2264,7 +2288,7 @@ async def auto_shutdown(ctx, to_cancel=None):
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
ctx.logger.info("Shutting down due to inactivity.")
while not ctx.exit_event.is_set():
if not ctx.client_activity_timers.values():

View File

@@ -21,6 +21,10 @@ if typing.TYPE_CHECKING:
import pathlib
class OptionError(ValueError):
pass
class Visibility(enum.IntFlag):
none = 0b0000
template = 0b0001
@@ -136,12 +140,6 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
def current_key(self) -> str:
return self.name_lookup[self.value]
def get_current_option_name(self) -> str:
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
f" use current_option_name instead. Worlds should use {self}.current_key"))
return self.current_option_name
@property
def current_option_name(self) -> str:
"""For display purposes. Worlds should be using current_key."""
@@ -746,37 +744,6 @@ class NamedRange(Range):
return super().from_text(text)
class SpecialRange(NamedRange):
special_range_cutoff = 0
# TODO: remove class SpecialRange, earliest 3 releases after 0.4.3
def __new__(cls, value: int) -> SpecialRange:
from Utils import deprecate
deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. "
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
"placed anywhere (below, inside, or above the regular range).")
return super().__new__(cls)
@classmethod
def weighted_range(cls, text) -> Range:
if text == "random-low":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.special_range_cutoff))
elif text == "random-high":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.range_end))
elif text == "random-middle":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end))
elif text.startswith("random-range-"):
return cls.custom_range(text)
elif text == "random":
return cls(random.randint(cls.special_range_cutoff, cls.range_end))
else:
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
f"Acceptable values are: random, random-high, random-middle, random-low, "
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
class FreezeValidKeys(AssembleOptions):
def __new__(mcs, name, bases, attrs):
if "valid_keys" in attrs:
@@ -980,7 +947,7 @@ class CommonOptions(metaclass=OptionsMetaProperty):
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
"""
Returns a dictionary of [str, Option.value]
:param option_names: names of the options to return
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
"""
@@ -1156,6 +1123,14 @@ class DeathLinkMixin:
death_link: DeathLink
class OptionGroup(typing.NamedTuple):
"""Define a grouping of options."""
name: str
"""Name of the group to categorize these options in for display on the WebHost and in generated YAMLS."""
options: typing.List[typing.Type[Option[typing.Any]]]
"""Options to be in the defined group."""
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
import os
@@ -1194,15 +1169,21 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
all_options: typing.Dict[str, AssembleOptions] = {
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
if option.visibility & Visibility.template
}
option_groups = {option: option_group.name
for option_group in world.web.option_groups
for option in option_group.options}
ordered_groups = ["Game Options"]
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
grouped_options = {group: {} for group in ordered_groups}
for option_name, option in world.options_dataclass.type_hints.items():
if option.visibility >= Visibility.template:
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
with open(local_path("data", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
options=all_options,
option_groups=grouped_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range,
)

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
@@ -83,6 +84,6 @@ def register():
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
app.register_blueprint(api.api_endpoints)

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import json
import logging
import multiprocessing
import threading
import time
import typing
from uuid import UUID
@@ -15,16 +14,6 @@ from Utils import restricted_loads
from .locker import Locker, AlreadyRunningException
def launch_room(room: Room, config: dict):
# requires db_session!
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
multiworld = multiworlds.get(room.id, None)
if not multiworld:
multiworld = MultiworldInstance(room, config)
multiworld.start()
def handle_generation_success(seed_id):
logging.info(f"Generation finished for seed {seed_id}")
@@ -59,21 +48,30 @@ def init_db(pony_config: dict):
db.generate_mapping()
def cleanup():
"""delete unowned user-content"""
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
# delete unowned user-content
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
run_guardian()
cleanup()
hosters = []
for x in range(config["HOSTERS"]):
hoster = MultiworldInstance(config, x)
hosters.append(hoster)
hoster.start()
while 1:
time.sleep(0.1)
with db_session:
@@ -81,7 +79,9 @@ def autohost(config: dict):
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
@@ -132,29 +132,38 @@ multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
class MultiworldInstance():
def __init__(self, room: Room, config: dict):
self.room_id = room.id
def __init__(self, config: dict, id: int):
self.room_ids = set()
self.process: typing.Optional[multiprocessing.Process] = None
with guardian_lock:
multiworlds[self.room_id] = self
self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
def start(self):
if self.process and self.process.is_alive():
return False
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host),
name="MultiHost")
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
# bind after start to prevent thread sync issues with guardian.
self.process = process
def start_room(self, room_id):
while not self.rooms_shutting_down.empty():
self.room_ids.remove(self.rooms_shutting_down.get(block=True, timeout=None))
if room_id in self.room_ids:
pass # should already be hosted currently.
else:
self.room_ids.add(room_id)
self.rooms_to_start.put(room_id)
def stop(self):
if self.process:
self.process.terminate()
@@ -168,40 +177,6 @@ class MultiworldInstance():
self.process = None
guardian = None
guardian_lock = threading.Lock()
def run_guardian():
global guardian
global multiworlds
with guardian_lock:
if not guardian:
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
def guard():
while 1:
time.sleep(1)
done = []
with guardian_lock:
for key, instance in multiworlds.items():
if instance.done():
instance.collect()
done.append(key)
for key in done:
del (multiworlds[key])
guardian = threading.Thread(name="Guardian", target=guard)
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@@ -5,6 +5,7 @@ import collections
import datetime
import functools
import logging
import multiprocessing
import pickle
import random
import socket
@@ -53,17 +54,19 @@ del MultiServer
class DBCommandProcessor(ServerCommandProcessor):
def output(self, text: str):
logging.info(text)
self.ctx.logger.info(text)
class WebHostContext(Context):
room_id: int
def __init__(self, static_server_data: dict):
def __init__(self, static_server_data: dict, logger: logging.Logger):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
super(WebHostContext, self).__init__("", 0, "", "", 1,
40, True, "enabled", "enabled",
"enabled", 0, 2, logger=logger)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
@@ -159,63 +162,95 @@ def get_static_server_data() -> dict:
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
def set_up_logging(room_id) -> logging.Logger:
import os
# logger setup
logger = logging.getLogger(f"RoomLogger {room_id}")
# this *should* be empty, but just in case.
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler.close()
file_handler = logging.FileHandler(
os.path.join(Utils.user_path("logs"), f"{room_id}.txt"),
"a",
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter("[%(asctime)s]: %(message)s"))
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)
return logger
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
Utils.init_logging(name)
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
del resource, file_limit
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
import gc
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
gc.collect() # free intermediate objects used during setup
import gc
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
del cert_file, cert_key_file, ponyconfig
gc.collect() # free intermediate objects used during setup
loop = asyncio.get_event_loop()
async def start_room(room_id):
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
logger = set_up_logging(room_id)
ctx = WebHostContext(static_server_data, logger)
ctx.load(room_id)
ctx.init_save()
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
port = socketname[1]
if port:
logging.info(f'Hosting game at {host}:{port}')
if port:
ctx.logger.info(f'Hosting game at {host}:{port}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
# ensure auto launch is on the same page in regard to room activity.
with db_session:
room: Room = Room.get(id=ctx.room_id)
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
# ensure auto launch is on the same page in regard to room activity.
with db_session:
room: Room = Room.get(id=ctx.room_id)
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
logging.info("Shutting down")
with Locker(room_id):
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
with db_session:
room = Room.get(id=room_id)
@@ -228,3 +263,17 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
raise
finally:
rooms_shutting_down.put(room_id)
class Starter(threading.Thread):
def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
logging.info(f"Starting room {next_room} on {name}.")
starter = Starter()
starter.daemon = True
starter.start()
loop.run_forever()

View File

@@ -37,25 +37,6 @@ def start_playing():
return render_template(f"startPlaying.html")
# TODO for back compat. remove around 0.4.5
@app.route("/weighted-settings")
def weighted_settings():
return redirect("weighted-options", 301)
@app.route("/weighted-options")
@cache.cached()
def weighted_options():
return render_template("weighted-options.html")
# Player options pages
@app.route("/games/<string:game>/player-options")
@cache.cached()
def player_options(game: str):
return render_template("player-options.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
@cache.cached()
@@ -150,6 +131,7 @@ def host_room(room: UUID):
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port

View File

@@ -1,14 +1,18 @@
import json
import logging
import collections.abc
import os
import typing
import yaml
import requests
import json
import flask
from urllib.parse import urlparse
import Options
from Utils import local_path
from Options import Visibility
from flask import redirect, render_template, request, Response
from worlds.AutoWorld import AutoWorldRegister
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
"exclude_locations", "priority_locations"}
from Utils import local_path
from textwrap import dedent
from . import app, cache
def create():
@@ -17,189 +21,233 @@ def create():
Options.generate_yaml_templates(yaml_folder)
def get_html_doc(option_type: type(Options.Option)) -> str:
if not option_type.__doc__:
return "Please document me!"
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
weighted_options = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"name": "",
"game": {},
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
def render_options_page(template: str, world_name: str, is_complex: bool = False):
world = AutoWorldRegister.world_types[world_name]
if world.hidden or world.web.options_page is False:
return redirect("games")
option_groups = {option: option_group.name
for option_group in world.web.option_groups
for option in option_group.options}
ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]]
grouped_options = {group: {} for group in ordered_groups}
for option_name, option in world.options_dataclass.type_hints.items():
# Exclude settings from options pages if their visibility is disabled
if not is_complex and option.visibility < Visibility.simple_ui:
continue
if is_complex and option.visibility < Visibility.complex_ui:
continue
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
return render_template(
template,
world_name=world_name,
world=world,
option_groups=grouped_options,
issubclass=issubclass,
Options=Options,
theme=get_world_theme(world_name),
)
def generate_game(player_name: str, formatted_options: dict):
payload = {
"race": 0,
"hint_cost": 10,
"forfeit_mode": "auto",
"remaining_mode": "disabled",
"collect_mode": "goal",
"weights": {
player_name: formatted_options,
},
"games": {},
}
for game_name, world in AutoWorldRegister.world_types.items():
url = urlparse(request.base_url)
port_string = f":{url.port}" if url.port else ""
r = requests.post(f"{url.scheme}://{url.hostname}{port_string}/api/generate", json=payload)
if 200 <= r.status_code <= 299:
response_data = r.json()
return redirect(response_data["url"])
else:
return r.text
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
# Generate JSON files for player-options pages
player_options = {
"baseOptions": {
"description": f"Generated by https://archipelago.gg/ for {game_name}",
"game": game_name,
"name": "",
},
}
def send_yaml(player_name: str, formatted_options: dict):
response = Response(yaml.dump(formatted_options, sort_keys=False))
response.headers["Content-Type"] = "text/yaml"
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
return response
game_options = {}
visible: typing.Set[str] = set()
visible_weighted: typing.Set[str] = set()
for option_name, option in all_options.items():
if option.visibility & Options.Visibility.simple_ui:
visible.add(option_name)
if option.visibility & Options.Visibility.complex_ui:
visible_weighted.add(option_name)
@app.template_filter("dedent")
def filter_dedent(text: str):
return dedent(text).strip("\n ")
if option_name in handled_in_js:
pass
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": None,
"options": []
}
@app.template_test("ordered")
def test_ordered(obj):
return isinstance(obj, collections.abc.Sequence)
for sub_option_id, sub_option_name in option.name_lookup.items():
if sub_option_name != "random":
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})
if sub_option_id == option.default:
this_option["defaultValue"] = sub_option_name
if not this_option["defaultValue"]:
this_option["defaultValue"] = "random"
@app.route("/games/<string:game>/option-presets", methods=["GET"])
@cache.cached()
def option_presets(game: str) -> Response:
world = AutoWorldRegister.world_types[game]
presets = {}
elif issubclass(option, Options.Range):
game_options[option_name] = {
"type": "range",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": option.default if hasattr(
option, "default") and option.default != "random" else option.range_start,
"min": option.range_start,
"max": option.range_end,
}
if world.web.options_presets:
presets = presets | world.web.options_presets
if issubclass(option, Options.NamedRange):
game_options[option_name]["type"] = 'named_range'
game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items():
game_options[option_name]["value_names"][key] = val
class SetEncoder(json.JSONEncoder):
def default(self, obj):
from collections.abc import Set
if isinstance(obj, Set):
return list(obj)
return json.JSONEncoder.default(self, obj)
elif issubclass(option, Options.ItemSet):
game_options[option_name] = {
"type": "items-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": list(option.default)
}
json_data = json.dumps(presets, cls=SetEncoder)
response = flask.Response(json_data)
response.headers["Content-Type"] = "application/json"
return response
elif issubclass(option, Options.LocationSet):
game_options[option_name] = {
"type": "locations-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": list(option.default)
}
elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict):
if option.valid_keys:
game_options[option_name] = {
"type": "custom-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"options": list(option.valid_keys),
"defaultValue": list(option.default) if hasattr(option, "default") else []
}
@app.route("/weighted-options")
def weighted_options_old():
return redirect("games", 301)
else:
logging.debug(f"{option} not exported to Web Options.")
player_options["presetOptions"] = {}
for preset_name, preset in world.web.options_presets.items():
player_options["presetOptions"][preset_name] = {}
for option_name, option_value in preset.items():
# Random range type settings are not valid.
assert (not str(option_value).startswith("random-")), \
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
f"values are not supported for presets."
@app.route("/games/<string:game>/weighted-options")
@cache.cached()
def weighted_options(game: str):
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
# Normal random is supported, but needs to be handled explicitly.
if option_value == "random":
player_options["presetOptions"][preset_name][option_name] = option_value
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
def generate_weighted_yaml(game: str):
if request.method == "POST":
intent_generate = False
options = {}
for key, val in request.form.items():
if "||" not in key:
if len(str(val)) == 0:
continue
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
if isinstance(option, Options.NamedRange) and isinstance(option_value, str):
assert option_value in option.special_range_names, \
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
options[key] = val
else:
if int(val) == 0:
continue
# Still use the true value for the option, not the name.
player_options["presetOptions"][preset_name][option_name] = option.value
elif isinstance(option, Options.Range):
player_options["presetOptions"][preset_name][option_name] = option.value
elif isinstance(option_value, str):
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
# setting a preset for an option with an overridden from_text method that would normally be okay,
# but would not be okay for the webhost's current implementation of player options UI.
assert option.name_lookup[option.value] == option_value, \
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
f"Values must not be resolved to a different option via option.from_text (or an alias)."
player_options["presetOptions"][preset_name][option_name] = option.current_key
else:
# int and bool values are fine, just resolve them to the current key for webhost.
player_options["presetOptions"][preset_name][option_name] = option.current_key
[option, setting] = key.split("||")
options.setdefault(option, {})[setting] = int(val)
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
# Error checking
if "name" not in options:
return "Player name is required."
filtered_player_options = player_options
filtered_player_options["gameOptions"] = {
option_name: option_data for option_name, option_data in game_options.items()
if option_name in visible
# Remove POST data irrelevant to YAML
if "intent-generate" in options:
intent_generate = True
del options["intent-generate"]
if "intent-export" in options:
del options["intent-export"]
# Properly format YAML output
player_name = options["name"]
del options["name"]
formatted_options = {
"name": player_name,
"game": game,
"description": f"Generated by https://archipelago.gg/ for {game}",
game: options,
}
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
if intent_generate:
return generate_game(player_name, formatted_options)
filtered_player_options["gameOptions"] = {
option_name: option_data for option_name, option_data in game_options.items()
if option_name in visible_weighted
else:
return send_yaml(player_name, formatted_options)
# Player options pages
@app.route("/games/<string:game>/player-options")
@cache.cached()
def player_options(game: str):
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
# YAML generator for player-options
@app.route("/games/<string:game>/generate-yaml", methods=["POST"])
def generate_yaml(game: str):
if request.method == "POST":
options = {}
intent_generate = False
for key, val in request.form.items(multi=True):
if key in options:
if not isinstance(options[key], list):
options[key] = [options[key]]
options[key].append(val)
else:
options[key] = val
# Detect and build ItemDict options from their name pattern
for key, val in options.copy().items():
key_parts = key.rsplit("||", 2)
if key_parts[-1] == "qty":
if key_parts[0] not in options:
options[key_parts[0]] = {}
if val != "0":
options[key_parts[0]][key_parts[1]] = int(val)
del options[key]
# Detect random-* keys and set their options accordingly
for key, val in options.copy().items():
if key.startswith("random-"):
options[key.removeprefix("random-")] = "random"
del options[key]
# Error checking
if not options["name"]:
return "Player name is required."
# Remove POST data irrelevant to YAML
preset_name = 'default'
if "intent-generate" in options:
intent_generate = True
del options["intent-generate"]
if "intent-export" in options:
del options["intent-export"]
if "game-options-preset" in options:
preset_name = options["game-options-preset"]
del options["game-options-preset"]
# Properly format YAML output
player_name = options["name"]
del options["name"]
description = f"Generated by https://archipelago.gg/ for {game}"
if preset_name != 'default' and preset_name != 'custom':
description += f" using {preset_name} preset"
formatted_options = {
"name": player_name,
"game": game,
"description": description,
game: options,
}
if not world.hidden and world.web.options_page is True:
# Add the random option to Choice, TextChoice, and Toggle options
for option in filtered_player_options["gameOptions"].values():
if option["type"] == "select":
option["options"].append({"name": "Random", "value": "random"})
if not option["defaultValue"]:
option["defaultValue"] = "random"
weighted_options["baseOptions"]["game"][game_name] = 0
weighted_options["games"][game_name] = {
"gameSettings": filtered_player_options["gameOptions"],
"gameItems": tuple(world.item_names),
"gameItemGroups": [
group for group in world.item_name_groups.keys() if group != "Everything"
],
"gameItemDescriptions": world.item_descriptions,
"gameLocations": tuple(world.location_names),
"gameLocationGroups": [
group for group in world.location_name_groups.keys() if group != "Everywhere"
],
"gameLocationDescriptions": world.location_descriptions,
}
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
if intent_generate:
return generate_game(player_name, formatted_options)
else:
return send_yaml(player_name, formatted_options)

View File

@@ -1,523 +0,0 @@
let gameName = null;
window.addEventListener('load', () => {
gameName = document.getElementById('player-options').getAttribute('data-game');
// Update game name on page
document.getElementById('game-name').innerText = gameName;
fetchOptionData().then((results) => {
let optionHash = localStorage.getItem(`${gameName}-hash`);
if (!optionHash) {
// If no hash data has been set before, set it now
optionHash = md5(JSON.stringify(results));
localStorage.setItem(`${gameName}-hash`, optionHash);
localStorage.removeItem(gameName);
}
if (optionHash !== md5(JSON.stringify(results))) {
showUserMessage(
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
);
document.getElementById('user-message').addEventListener('click', resetOptions);
}
// Page setup
createDefaultOptions(results);
buildUI(results);
adjustHeaderWidth();
// Event listeners
document.getElementById('export-options').addEventListener('click', () => exportOptions());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
const playerOptions = JSON.parse(localStorage.getItem(gameName));
const nameInput = document.getElementById('player-name');
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
nameInput.value = playerOptions.name;
// Presets
const presetSelect = document.getElementById('game-options-preset');
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
for (const preset in results['presetOptions']) {
const presetOption = document.createElement('option');
presetOption.innerText = preset;
presetSelect.appendChild(presetOption);
}
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
results['presetOptions']['__default'] = {};
}).catch((e) => {
console.error(e);
const url = new URL(window.location.href);
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
})
});
const resetOptions = () => {
localStorage.removeItem(gameName);
localStorage.removeItem(`${gameName}-hash`);
localStorage.removeItem(`${gameName}-preset`);
window.location.reload();
};
const fetchOptionData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject(ajax.responseText);
return;
}
try{ resolve(JSON.parse(ajax.responseText)); }
catch(error){ reject(error); }
};
ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true);
ajax.send();
});
const createDefaultOptions = (optionData) => {
if (!localStorage.getItem(gameName)) {
const newOptions = {
[gameName]: {},
};
for (let baseOption of Object.keys(optionData.baseOptions)){
newOptions[baseOption] = optionData.baseOptions[baseOption];
}
for (let gameOption of Object.keys(optionData.gameOptions)){
newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue;
}
localStorage.setItem(gameName, JSON.stringify(newOptions));
}
if (!localStorage.getItem(`${gameName}-preset`)) {
localStorage.setItem(`${gameName}-preset`, '__default');
}
};
const buildUI = (optionData) => {
// Game Options
const leftGameOpts = {};
const rightGameOpts = {};
Object.keys(optionData.gameOptions).forEach((key, index) => {
if (index < Object.keys(optionData.gameOptions).length / 2) {
leftGameOpts[key] = optionData.gameOptions[key];
} else {
rightGameOpts[key] = optionData.gameOptions[key];
}
});
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
};
const buildOptionsTable = (options, romOpts = false) => {
const currentOptions = JSON.parse(localStorage.getItem(gameName));
const table = document.createElement('table');
const tbody = document.createElement('tbody');
Object.keys(options).forEach((option) => {
const tr = document.createElement('tr');
// td Left
const tdl = document.createElement('td');
const label = document.createElement('label');
label.textContent = `${options[option].displayName}: `;
label.setAttribute('for', option);
const questionSpan = document.createElement('span');
questionSpan.classList.add('interactive');
questionSpan.setAttribute('data-tooltip', options[option].description);
questionSpan.innerText = '(?)';
label.appendChild(questionSpan);
tdl.appendChild(label);
tr.appendChild(tdl);
// td Right
const tdr = document.createElement('td');
let element = null;
const randomButton = document.createElement('button');
switch(options[option].type) {
case 'select':
element = document.createElement('div');
element.classList.add('select-container');
let select = document.createElement('select');
select.setAttribute('id', option);
select.setAttribute('data-key', option);
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
options[option].options.forEach((opt) => {
const optionElement = document.createElement('option');
optionElement.setAttribute('value', opt.value);
optionElement.innerText = opt.name;
if ((isNaN(currentOptions[gameName][option]) &&
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
(opt.value === currentOptions[gameName][option]))
{
optionElement.selected = true;
}
select.appendChild(optionElement);
});
select.addEventListener('change', (event) => updateGameOption(event.target));
element.appendChild(select);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', option);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
if (currentOptions[gameName][option] === 'random') {
randomButton.classList.add('active');
select.disabled = true;
}
element.appendChild(randomButton);
break;
case 'range':
element = document.createElement('div');
element.classList.add('range-container');
let range = document.createElement('input');
range.setAttribute('id', option);
range.setAttribute('type', 'range');
range.setAttribute('data-key', option);
range.setAttribute('min', options[option].min);
range.setAttribute('max', options[option].max);
range.value = currentOptions[gameName][option];
range.addEventListener('change', (event) => {
document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target);
});
element.appendChild(range);
let rangeVal = document.createElement('span');
rangeVal.classList.add('range-value');
rangeVal.setAttribute('id', `${option}-value`);
rangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
currentOptions[gameName][option] : options[option].defaultValue;
element.appendChild(rangeVal);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', option);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
if (currentOptions[gameName][option] === 'random') {
randomButton.classList.add('active');
range.disabled = true;
}
element.appendChild(randomButton);
break;
case 'named_range':
element = document.createElement('div');
element.classList.add('named-range-container');
// Build the select element
let namedRangeSelect = document.createElement('select');
namedRangeSelect.setAttribute('data-key', option);
Object.keys(options[option].value_names).forEach((presetName) => {
let presetOption = document.createElement('option');
presetOption.innerText = presetName;
presetOption.value = options[option].value_names[presetName];
const words = presetOption.innerText.split('_');
for (let i = 0; i < words.length; i++) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
presetOption.innerText = words.join(' ');
namedRangeSelect.appendChild(presetOption);
});
let customOption = document.createElement('option');
customOption.innerText = 'Custom';
customOption.value = 'custom';
customOption.selected = true;
namedRangeSelect.appendChild(customOption);
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
namedRangeSelect.value = Number(currentOptions[gameName][option]);
}
// Build range element
let namedRangeWrapper = document.createElement('div');
namedRangeWrapper.classList.add('named-range-wrapper');
let namedRange = document.createElement('input');
namedRange.setAttribute('type', 'range');
namedRange.setAttribute('data-key', option);
namedRange.setAttribute('min', options[option].min);
namedRange.setAttribute('max', options[option].max);
namedRange.value = currentOptions[gameName][option];
// Build rage value element
let namedRangeVal = document.createElement('span');
namedRangeVal.classList.add('range-value');
namedRangeVal.setAttribute('id', `${option}-value`);
namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
currentOptions[gameName][option] : options[option].defaultValue;
// Configure select event listener
namedRangeSelect.addEventListener('change', (event) => {
if (event.target.value === 'custom') { return; }
// Update range slider
namedRange.value = event.target.value;
document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target);
});
// Configure range event handler
namedRange.addEventListener('change', (event) => {
// Update select element
namedRangeSelect.value =
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
parseInt(event.target.value) : 'custom';
document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target);
});
element.appendChild(namedRangeSelect);
namedRangeWrapper.appendChild(namedRange);
namedRangeWrapper.appendChild(namedRangeVal);
element.appendChild(namedRangeWrapper);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', option);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(
event, namedRange, namedRangeSelect)
);
if (currentOptions[gameName][option] === 'random') {
randomButton.classList.add('active');
namedRange.disabled = true;
namedRangeSelect.disabled = true;
}
namedRangeWrapper.appendChild(randomButton);
break;
default:
console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`);
return;
}
tdr.appendChild(element);
tr.appendChild(tdr);
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
};
const setPresets = (optionsData, presetName) => {
const defaults = optionsData['gameOptions'];
const preset = optionsData['presetOptions'][presetName];
localStorage.setItem(`${gameName}-preset`, presetName);
if (!preset) {
console.error(`No presets defined for preset name: '${presetName}'`);
return;
}
const updateOptionElement = (option, presetValue) => {
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
if (presetValue === 'random') {
randomElement.classList.add('active');
optionElement.disabled = true;
updateGameOption(randomElement, false);
} else {
optionElement.value = presetValue;
randomElement.classList.remove('active');
optionElement.disabled = undefined;
updateGameOption(optionElement, false);
}
};
for (const option in defaults) {
let presetValue = preset[option];
if (presetValue === undefined) {
// Using the default value if not set in presets.
presetValue = defaults[option]['defaultValue'];
}
switch (defaults[option].type) {
case 'range':
const numberElement = document.querySelector(`#${option}-value`);
if (presetValue === 'random') {
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
: defaults[option]['defaultValue'];
} else {
numberElement.innerText = presetValue;
}
updateOptionElement(option, presetValue);
break;
case 'select': {
updateOptionElement(option, presetValue);
break;
}
case 'named_range': {
const selectElement = document.querySelector(`select[data-key='${option}']`);
const rangeElement = document.querySelector(`input[data-key='${option}']`);
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
if (presetValue === 'random') {
randomElement.classList.add('active');
selectElement.disabled = true;
rangeElement.disabled = true;
updateGameOption(randomElement, false);
} else {
rangeElement.value = presetValue;
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
parseInt(presetValue) : 'custom';
document.getElementById(`${option}-value`).innerText = presetValue;
randomElement.classList.remove('active');
selectElement.disabled = undefined;
rangeElement.disabled = undefined;
updateGameOption(rangeElement, false);
}
break;
}
default:
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
break;
}
}
};
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
const active = event.target.classList.contains('active');
const randomButton = event.target;
if (active) {
randomButton.classList.remove('active');
inputElement.disabled = undefined;
if (optionalSelectElement) {
optionalSelectElement.disabled = undefined;
}
} else {
randomButton.classList.add('active');
inputElement.disabled = true;
if (optionalSelectElement) {
optionalSelectElement.disabled = true;
}
}
updateGameOption(active ? inputElement : randomButton);
};
const updateBaseOption = (event) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value);
localStorage.setItem(gameName, JSON.stringify(options));
};
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
const options = JSON.parse(localStorage.getItem(gameName));
if (toggleCustomPreset) {
localStorage.setItem(`${gameName}-preset`, '__custom');
const presetElement = document.getElementById('game-options-preset');
presetElement.value = '__custom';
}
if (optionElement.classList.contains('randomize-button')) {
// If the event passed in is the randomize button, then we know what we must do.
options[gameName][optionElement.getAttribute('data-key')] = 'random';
} else {
options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ?
optionElement.value : parseInt(optionElement.value, 10);
}
localStorage.setItem(gameName, JSON.stringify(options));
};
const exportOptions = () => {
const options = JSON.parse(localStorage.getItem(gameName));
const preset = localStorage.getItem(`${gameName}-preset`);
switch (preset) {
case '__default':
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
break;
case '__custom':
options['description'] = `Generated by https://archipelago.gg.`;
break;
default:
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
}
if (!options.name || options.name.toString().trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const generateGame = (raceMode = false) => {
const options = JSON.parse(localStorage.getItem(gameName));
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
axios.post('/api/generate', {
weights: { player: options },
presetData: { player: options },
playerCount: 1,
spoiler: 3,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
let userMessage = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage += ' ' + error.response.data.text;
}
showUserMessage(userMessage);
console.error(error);
});
};
const showUserMessage = (message) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = message;
userMessage.classList.add('visible');
window.scrollTo(0, 0);
userMessage.addEventListener('click', () => {
userMessage.classList.remove('visible');
userMessage.addEventListener('click', hideUserMessage);
});
};
const hideUserMessage = () => {
const userMessage = document.getElementById('user-message');
userMessage.classList.remove('visible');
userMessage.removeEventListener('click', hideUserMessage);
};

View File

@@ -0,0 +1,335 @@
let presets = {};
window.addEventListener('load', async () => {
// Load settings from localStorage, if available
loadSettings();
// Fetch presets if available
await fetchPresets();
// Handle changes to range inputs
document.querySelectorAll('input[type=range]').forEach((range) => {
const optionName = range.getAttribute('id');
range.addEventListener('change', () => {
document.getElementById(`${optionName}-value`).innerText = range.value;
// Handle updating named range selects to "custom" if appropriate
const select = document.querySelector(`select[data-option-name=${optionName}]`);
if (select) {
let updated = false;
select?.childNodes.forEach((option) => {
if (option.value === range.value) {
select.value = range.value;
updated = true;
}
});
if (!updated) {
select.value = 'custom';
}
}
});
});
// Handle changes to named range selects
document.querySelectorAll('.named-range-container select').forEach((select) => {
const optionName = select.getAttribute('data-option-name');
select.addEventListener('change', (evt) => {
document.getElementById(optionName).value = evt.target.value;
document.getElementById(`${optionName}-value`).innerText = evt.target.value;
});
});
// Handle changes to randomize checkboxes
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
const optionName = checkbox.getAttribute('data-option-name');
checkbox.addEventListener('change', () => {
const optionInput = document.getElementById(optionName);
const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`);
const customInput = document.getElementById(`${optionName}-custom`);
if (checkbox.checked) {
optionInput.setAttribute('disabled', '1');
namedRangeSelect?.setAttribute('disabled', '1');
if (customInput) {
customInput.setAttribute('disabled', '1');
}
} else {
optionInput.removeAttribute('disabled');
namedRangeSelect?.removeAttribute('disabled');
if (customInput) {
customInput.removeAttribute('disabled');
}
}
});
});
// Handle changes to TextChoice input[type=text]
document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => {
const optionName = input.getAttribute('data-option-name');
input.addEventListener('input', () => {
const select = document.getElementById(optionName);
const optionValues = [];
select.childNodes.forEach((option) => optionValues.push(option.value));
select.value = (optionValues.includes(input.value)) ? input.value : 'custom';
});
});
// Handle changes to TextChoice select
document.querySelectorAll('.text-choice-container select').forEach((select) => {
const optionName = select.getAttribute('id');
select.addEventListener('change', () => {
document.getElementById(`${optionName}-custom`).value = '';
});
});
// Update the "Option Preset" select to read "custom" when changes are made to relevant inputs
const presetSelect = document.getElementById('game-options-preset');
document.querySelectorAll('input, select').forEach((input) => {
if ( // Ignore inputs which have no effect on yaml generation
(input.id === 'player-name') ||
(input.id === 'game-options-preset') ||
(input.classList.contains('group-toggle')) ||
(input.type === 'submit')
) {
return;
}
input.addEventListener('change', () => {
presetSelect.value = 'custom';
});
});
// Handle changes to presets select
document.getElementById('game-options-preset').addEventListener('change', choosePreset);
// Save settings to localStorage when form is submitted
document.getElementById('options-form').addEventListener('submit', (evt) => {
const playerName = document.getElementById('player-name');
if (!playerName.value.trim()) {
evt.preventDefault();
window.scrollTo(0, 0);
showUserMessage('You must enter a player name!');
}
saveSettings();
});
});
// Save all settings to localStorage
const saveSettings = () => {
const options = {
inputs: {},
checkboxes: {},
};
document.querySelectorAll('input, select').forEach((input) => {
if (input.type === 'submit') {
// Ignore submit inputs
}
else if (input.type === 'checkbox') {
options.checkboxes[input.id] = input.checked;
}
else {
options.inputs[input.id] = input.value
}
});
const game = document.getElementById('player-options').getAttribute('data-game');
localStorage.setItem(game, JSON.stringify(options));
};
// Load all options from localStorage
const loadSettings = () => {
const game = document.getElementById('player-options').getAttribute('data-game');
const options = JSON.parse(localStorage.getItem(game));
if (options) {
if (!options.inputs || !options.checkboxes) {
localStorage.removeItem(game);
return;
}
// Restore value-based inputs and selects
Object.keys(options.inputs).forEach((key) => {
try{
document.getElementById(key).value = options.inputs[key];
const rangeValue = document.getElementById(`${key}-value`);
if (rangeValue) {
rangeValue.innerText = options.inputs[key];
}
} catch (err) {
console.error(`Unable to restore value to input with id ${key}`);
}
});
// Restore checkboxes
Object.keys(options.checkboxes).forEach((key) => {
try{
if (options.checkboxes[key]) {
document.getElementById(key).setAttribute('checked', '1');
}
} catch (err) {
console.error(`Unable to restore value to input with id ${key}`);
}
});
}
// Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
const optionName = checkbox.getAttribute('data-option-name');
if (checkbox.checked) {
const input = document.getElementById(optionName);
if (input) {
input.setAttribute('disabled', '1');
}
const customInput = document.getElementById(`${optionName}-custom`);
if (customInput) {
customInput.setAttribute('disabled', '1');
}
}
});
};
/**
* Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen
* @returns {Promise<void>}
*/
const fetchPresets = async () => {
const response = await fetch('option-presets');
presets = await response.json();
const presetSelect = document.getElementById('game-options-preset');
presetSelect.removeAttribute('disabled');
const game = document.getElementById('player-options').getAttribute('data-game');
const presetToApply = localStorage.getItem(`${game}-preset`);
const playerName = localStorage.getItem(`${game}-player`);
if (presetToApply) {
localStorage.removeItem(`${game}-preset`);
presetSelect.value = presetToApply;
applyPresets(presetToApply);
}
if (playerName) {
document.getElementById('player-name').value = playerName;
localStorage.removeItem(`${game}-player`);
}
};
/**
* Clear the localStorage for this game and set a preset to be loaded upon page reload
* @param evt
*/
const choosePreset = (evt) => {
if (evt.target.value === 'custom') { return; }
const game = document.getElementById('player-options').getAttribute('data-game');
localStorage.removeItem(game);
localStorage.setItem(`${game}-player`, document.getElementById('player-name').value);
if (evt.target.value !== 'default') {
localStorage.setItem(`${game}-preset`, evt.target.value);
}
document.querySelectorAll('#options-form input, #options-form select').forEach((input) => {
if (input.id === 'player-name') { return; }
input.removeAttribute('value');
});
window.location.replace(window.location.href);
};
const applyPresets = (presetName) => {
// Ignore the "default" preset, because it gets set automatically by Jinja
if (presetName === 'default') {
saveSettings();
return;
}
if (!presets[presetName]) {
console.error(`Unknown preset ${presetName} chosen`);
return;
}
const preset = presets[presetName];
Object.keys(preset).forEach((optionName) => {
const optionValue = preset[optionName];
// Handle List and Set options
if (Array.isArray(optionValue)) {
document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => {
if (optionValue.includes(checkbox.value)) {
checkbox.setAttribute('checked', '1');
} else {
checkbox.removeAttribute('checked');
}
});
return;
}
// Handle Dict options
if (typeof(optionValue) === 'object' && optionValue !== null) {
const itemNames = Object.keys(optionValue);
document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => {
const itemName = input.getAttribute('data-item-name');
input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0
});
return;
}
// Identify all possible elements
const normalInput = document.getElementById(optionName);
const customInput = document.getElementById(`${optionName}-custom`);
const rangeValue = document.getElementById(`${optionName}-value`);
const randomizeInput = document.getElementById(`random-${optionName}`);
const namedRangeSelect = document.getElementById(`${optionName}-select`);
// It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
let trueValue = optionValue;
if (namedRangeSelect) {
namedRangeSelect.querySelectorAll('option').forEach((opt) => {
if (opt.innerText.startsWith(optionValue)) {
trueValue = opt.value;
}
});
namedRangeSelect.value = trueValue;
}
// Handle options whose presets are "random"
if (optionValue === 'random') {
normalInput.setAttribute('disabled', '1');
randomizeInput.setAttribute('checked', '1');
if (customInput) {
customInput.setAttribute('disabled', '1');
}
if (rangeValue) {
rangeValue.innerText = normalInput.value;
}
if (namedRangeSelect) {
namedRangeSelect.setAttribute('disabled', '1');
}
return;
}
// Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
normalInput.value = trueValue;
normalInput.removeAttribute('disabled');
randomizeInput.removeAttribute('checked');
if (customInput) {
document.getElementById(`${optionName}-custom`).removeAttribute('disabled');
}
if (rangeValue) {
rangeValue.innerText = trueValue;
}
});
saveSettings();
};
const showUserMessage = (text) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = text;
userMessage.addEventListener('click', hideUserMessage);
userMessage.style.display = 'block';
};
const hideUserMessage = () => {
const userMessage = document.getElementById('user-message');
userMessage.removeEventListener('click', hideUserMessage);
userMessage.style.display = 'none';
};

View File

@@ -1,18 +1,16 @@
window.addEventListener('load', () => {
// Add toggle listener to all elements with .collapse-toggle
const toggleButtons = document.querySelectorAll('.collapse-toggle');
toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
const toggleButtons = document.querySelectorAll('details');
// Handle game filter input
const gameSearch = document.getElementById('game-search');
gameSearch.value = '';
gameSearch.addEventListener('input', (evt) => {
if (!evt.target.value.trim()) {
// If input is empty, display all collapsed games
// If input is empty, display all games as collapsed
return toggleButtons.forEach((header) => {
header.style.display = null;
header.firstElementChild.innerText = '▶';
header.nextElementSibling.classList.add('collapsed');
header.removeAttribute('open');
});
}
@@ -21,12 +19,10 @@ window.addEventListener('load', () => {
// If the game name includes the search string, display the game. If not, hide it
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
header.style.display = null;
header.firstElementChild.innerText = '▼';
header.nextElementSibling.classList.remove('collapsed');
header.setAttribute('open', '1');
} else {
header.style.display = 'none';
header.firstElementChild.innerText = '▶';
header.nextElementSibling.classList.add('collapsed');
header.removeAttribute('open');
}
});
});
@@ -35,30 +31,14 @@ window.addEventListener('load', () => {
document.getElementById('collapse-all').addEventListener('click', collapseAll);
});
const toggleCollapse = (evt) => {
const gameArrow = evt.target.firstElementChild;
const gameInfo = evt.target.nextElementSibling;
if (gameInfo.classList.contains('collapsed')) {
gameArrow.innerText = '▼';
gameInfo.classList.remove('collapsed');
} else {
gameArrow.innerText = '▶';
gameInfo.classList.add('collapsed');
}
};
const expandAll = () => {
document.querySelectorAll('.collapse-toggle').forEach((header) => {
if (header.style.display === 'none') { return; }
header.firstElementChild.innerText = '▼';
header.nextElementSibling.classList.remove('collapsed');
document.querySelectorAll('details').forEach((detail) => {
detail.setAttribute('open', '1');
});
};
const collapseAll = () => {
document.querySelectorAll('.collapse-toggle').forEach((header) => {
if (header.style.display === 'none') { return; }
header.firstElementChild.innerText = '▶';
header.nextElementSibling.classList.add('collapsed');
document.querySelectorAll('details').forEach((detail) => {
detail.removeAttribute('open');
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
let deletedOptions = {};
window.addEventListener('load', () => {
const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
// Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
// and handles dynamically created elements
document.addEventListener('change', (evt) => {
// Handle updates to range inputs
if (evt.target.type === 'range') {
// Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
// If the changed option was the name of a game, determine whether to show or hide that game's div
if (evt.target.id.startsWith('game||')) {
const gameName = evt.target.id.split('||')[1];
const gameDiv = document.getElementById(`${gameName}-container`);
if (evt.target.value > 0) {
gameDiv.classList.remove('hidden');
} else {
gameDiv.classList.add('hidden');
}
}
}
});
// Generic click listener
document.addEventListener('click', (evt) => {
// Handle creating new rows for Range options
if (evt.target.classList.contains('add-range-option-button')) {
const optionName = evt.target.getAttribute('data-option');
addRangeRow(optionName);
}
// Handle deleting range rows
if (evt.target.classList.contains('range-option-delete')) {
const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
setDeletedOption(
targetRow.getAttribute('data-option-name'),
targetRow.getAttribute('data-value'),
);
targetRow.parentElement.removeChild(targetRow);
}
});
// Listen for enter presses on inputs intended to add range rows
document.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') {
evt.preventDefault();
}
if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
const optionName = evt.target.getAttribute('data-option');
addRangeRow(optionName);
}
});
// Detect form submission
document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
// Save data to localStorage
const weightedOptions = {};
document.querySelectorAll('input[name]').forEach((input) => {
const keys = input.getAttribute('name').split('||');
// Determine keys
const optionName = keys[0] ?? null;
const subOption = keys[1] ?? null;
// Ensure keys exist
if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
if (subOption && !weightedOptions[optionName][subOption]) {
weightedOptions[optionName][subOption] = null;
}
if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
if (optionName) { return weightedOptions[optionName] = determineValue(input); }
});
localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
});
// Remove all deleted values as specified by localStorage
deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
Object.keys(deletedOptions).forEach((optionName) => {
deletedOptions[optionName].forEach((value) => {
const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
targetRow.parentElement.removeChild(targetRow);
});
});
// Populate all settings from localStorage on page initialisation
const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
if (previousSettingsJson) {
const previousSettings = JSON.parse(previousSettingsJson);
Object.keys(previousSettings).forEach((option) => {
if (typeof previousSettings[option] === 'string') {
return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
}
Object.keys(previousSettings[option]).forEach((value) => {
const input = document.querySelector(`input[name="${option}||${value}"]`);
if (!input?.type) {
return console.error(`Unable to populate option with name ${option}||${value}.`);
}
switch (input.type) {
case 'checkbox':
input.checked = (parseInt(previousSettings[option][value], 10) === 1);
break;
case 'range':
input.value = parseInt(previousSettings[option][value], 10);
break;
case 'number':
input.value = previousSettings[option][value].toString();
break;
default:
console.error(`Found unsupported input type: ${input.type}`);
}
});
});
}
});
const addRangeRow = (optionName) => {
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
const inputTarget = document.querySelector(inputQuery);
const newValue = inputTarget.value;
if (!/^-?\d+$/.test(newValue)) {
alert('Range values must be a positive or negative integer!');
return;
}
inputTarget.value = '';
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
const tr = document.createElement('tr');
tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
tr.setAttribute('data-option-name', optionName);
tr.setAttribute('data-value', newValue);
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
const label = document.createElement('label');
label.setAttribute('for', `${optionName}||${newValue}`);
label.innerText = newValue.toString();
tdLeft.appendChild(label);
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('min', '0');
range.setAttribute('max', '50');
range.setAttribute('value', '0');
range.setAttribute('id', `${optionName}||${newValue}`);
range.setAttribute('name', `${optionName}||${newValue}`);
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.classList.add('td-right');
const valueSpan = document.createElement('span');
valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
valueSpan.innerText = '0';
tdRight.appendChild(valueSpan);
tr.appendChild(tdRight);
const tdDelete = document.createElement('td');
const deleteSpan = document.createElement('span');
deleteSpan.classList.add('range-option-delete');
deleteSpan.classList.add('js-required');
deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
deleteSpan.innerText = '❌';
tdDelete.appendChild(deleteSpan);
tr.appendChild(tdDelete);
tBody.appendChild(tr);
// Remove this option from the set of deleted options if it exists
unsetDeletedOption(optionName, newValue);
};
/**
* Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
*
* @param {object} input - The input element.
* @returns {number} The value of the input element.
*/
const determineValue = (input) => {
switch (input.type) {
case 'checkbox':
return (input.checked ? 1 : 0);
case 'range':
return parseInt(input.value, 10);
default:
return input.value;
}
};
/**
* Sets the deleted option value for a given world and option name.
* If the world or option does not exist, it creates the necessary entries.
*
* @param {string} optionName - The name of the option.
* @param {*} value - The value to be set for the deleted option.
* @returns {void}
*/
const setDeletedOption = (optionName, value) => {
deletedOptions[optionName] = deletedOptions[optionName] || [];
deletedOptions[optionName].push(`${optionName}-${value}`);
};
/**
* Removes a specific value from the deletedOptions object.
*
* @param {string} optionName - The name of the option.
* @param {*} value - The value to be removed
* @returns {void}
*/
const unsetDeletedOption = (optionName, value) => {
if (!deletedOptions.hasOwnProperty(optionName)) { return; }
if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
}
if (deletedOptions[optionName].length === 0) {
delete deletedOptions[optionName];
}
};

View File

@@ -44,7 +44,7 @@ a{
font-family: LexendDeca-Regular, sans-serif;
}
button{
button, input[type=submit]{
font-weight: 500;
font-size: 0.9rem;
padding: 10px 17px 11px 16px; /* top right bottom left */
@@ -57,7 +57,7 @@ button{
cursor: pointer;
}
button:active{
button:active, input[type=submit]:active{
border-right: 1px solid rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
padding-right: 16px;
@@ -66,11 +66,11 @@ button:active{
margin-bottom: 2px;
}
button.button-grass{
button.button-grass, input[type=submit].button-grass{
border: 1px solid black;
}
button.button-dirt{
button.button-dirt, input[type=submit].button-dirt{
border: 1px solid black;
}
@@ -111,4 +111,4 @@ h5, h6{
.interactive{
color: #ffef00;
}
}

View File

@@ -23,7 +23,7 @@
.markdown a{}
.markdown h1{
.markdown h1, .markdown details summary.h1{
font-size: 52px;
font-weight: normal;
font-family: LondrinaSolid-Regular, sans-serif;
@@ -33,7 +33,7 @@
text-shadow: 1px 1px 4px #000000;
}
.markdown h2{
.markdown h2, .markdown details summary.h2{
font-size: 38px;
font-weight: normal;
font-family: LondrinaSolid-Light, sans-serif;
@@ -45,7 +45,7 @@
text-shadow: 1px 1px 2px #000000;
}
.markdown h3{
.markdown h3, .markdown details summary.h3{
font-size: 26px;
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
@@ -55,7 +55,7 @@
margin-bottom: 0.5rem;
}
.markdown h4{
.markdown h4, .markdown details summary.h4{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 24px;
@@ -63,21 +63,21 @@
margin-bottom: 24px;
}
.markdown h5{
.markdown h5, .markdown details summary.h5{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 22px;
cursor: pointer;
}
.markdown h6{
.markdown h6, .markdown details summary.h6{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 20px;
cursor: pointer;;
}
.markdown h4, .markdown h5,.markdown h6{
.markdown h4, .markdown h5, .markdown h6{
margin-bottom: 0.5rem;
}

View File

@@ -1,244 +0,0 @@
html{
background-image: url('../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#player-options{
box-sizing: border-box;
max-width: 1024px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#player-options #player-options-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-options code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#player-options #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#player-options #user-message.visible{
display: block;
cursor: pointer;
}
#player-options h1{
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 4px #000000;
}
#player-options h2{
font-size: 40px;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-options h3, #player-options h4, #player-options h5, #player-options h6{
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-options input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-options input:not([type]):focus{
border: 1px solid #ffffff;
}
#player-options select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#player-options #game-options, #player-options #rom-options{
display: flex;
flex-direction: row;
}
#player-options #meta-options {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 3px;
}
#player-options div {
display: flex;
flex-grow: 1;
}
#player-options #meta-options label {
display: inline-block;
min-width: 180px;
flex-grow: 1;
}
#player-options #meta-options input,
#player-options #meta-options select {
box-sizing: border-box;
min-width: 150px;
width: 50%;
}
#player-options .left, #player-options .right{
flex-grow: 1;
}
#player-options .left{
margin-right: 10px;
}
#player-options .right{
margin-left: 10px;
}
#player-options table{
margin-bottom: 30px;
width: 100%;
}
#player-options table .select-container{
display: flex;
flex-direction: row;
}
#player-options table .select-container select{
min-width: 200px;
flex-grow: 1;
}
#player-options table select:disabled{
background-color: lightgray;
}
#player-options table .range-container{
display: flex;
flex-direction: row;
}
#player-options table .range-container input[type=range]{
flex-grow: 1;
}
#player-options table .range-value{
min-width: 20px;
margin-left: 0.25rem;
}
#player-options table .named-range-container{
display: flex;
flex-direction: column;
}
#player-options table .named-range-wrapper{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#player-options table .named-range-wrapper input[type=range]{
flex-grow: 1;
}
#player-options table .randomize-button {
max-height: 24px;
line-height: 16px;
padding: 2px 8px;
margin: 0 0 0 0.25rem;
font-size: 12px;
border: 1px solid black;
border-radius: 3px;
}
#player-options table .randomize-button.active {
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
}
#player-options table .randomize-button[data-tooltip]::after {
left: unset;
right: 0;
}
#player-options table label{
display: block;
min-width: 200px;
margin-right: 4px;
cursor: default;
}
#player-options th, #player-options td{
border: none;
padding: 3px;
font-size: 17px;
vertical-align: top;
}
@media all and (max-width: 1024px) {
#player-options {
border-radius: 0;
}
#player-options #meta-options {
flex-direction: column;
justify-content: flex-start;
gap: 6px;
}
#player-options #game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-options .left,
#player-options .right {
margin: 0;
}
#game-options table {
margin-bottom: 0;
}
#game-options table label{
display: block;
min-width: 200px;
}
#game-options table tr td {
width: 50%;
}
}

View File

@@ -0,0 +1,310 @@
@import "../markdown.css";
html {
background-image: url("../../static/backgrounds/grass.png");
background-repeat: repeat;
background-size: 650px 650px;
overflow-x: hidden;
}
#player-options {
box-sizing: border-box;
max-width: 1024px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
word-break: break-all;
}
#player-options #player-options-header h1 {
margin-bottom: 0;
padding-bottom: 0;
}
#player-options #player-options-header h1:nth-child(2) {
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
#player-options .js-warning-banner {
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
#player-options .group-container {
padding: 0;
margin: 0;
}
#player-options .group-container h2 {
user-select: none;
cursor: unset;
}
#player-options .group-container h2 label {
cursor: pointer;
}
#player-options #player-options-button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-options #user-message {
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
cursor: pointer;
}
#player-options h1 {
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 4px #000000;
}
#player-options h2 {
font-size: 40px;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-options h3, #player-options h4, #player-options h5, #player-options h6 {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-options input:not([type]) {
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-options input:not([type]):focus {
border: 1px solid #ffffff;
}
#player-options select {
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
text-overflow: ellipsis;
}
#player-options .game-options {
display: flex;
flex-direction: row;
}
#player-options .game-options .left, #player-options .game-options .right {
display: grid;
grid-template-columns: 12rem auto;
grid-row-gap: 0.5rem;
grid-auto-rows: min-content;
align-items: start;
min-width: 480px;
width: 50%;
}
#player-options #meta-options {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 3px;
}
#player-options #meta-options input, #player-options #meta-options select {
box-sizing: border-box;
width: 200px;
}
#player-options .left, #player-options .right {
flex-grow: 1;
margin-bottom: 0.5rem;
}
#player-options .left {
margin-right: 20px;
}
#player-options .select-container {
display: flex;
flex-direction: row;
max-width: 270px;
}
#player-options .select-container select {
min-width: 200px;
flex-grow: 1;
}
#player-options .select-container select:disabled {
background-color: lightgray;
}
#player-options .range-container {
display: flex;
flex-direction: row;
max-width: 270px;
}
#player-options .range-container input[type=range] {
flex-grow: 1;
}
#player-options .range-container .range-value {
min-width: 20px;
margin-left: 0.25rem;
}
#player-options .named-range-container {
display: flex;
flex-direction: column;
max-width: 270px;
}
#player-options .named-range-container .named-range-wrapper {
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#player-options .named-range-container .named-range-wrapper input[type=range] {
flex-grow: 1;
}
#player-options .free-text-container {
display: flex;
flex-direction: column;
max-width: 270px;
}
#player-options .free-text-container input[type=text] {
flex-grow: 1;
}
#player-options .text-choice-container {
display: flex;
flex-direction: column;
max-width: 270px;
}
#player-options .text-choice-container .text-choice-wrapper {
display: flex;
flex-direction: row;
margin-bottom: 0.25rem;
}
#player-options .text-choice-container .text-choice-wrapper select {
flex-grow: 1;
}
#player-options .option-container {
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 10rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
}
#player-options .option-container .option-divider {
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
#player-options .option-container .option-entry {
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 0.125rem;
margin-top: 0.125rem;
user-select: none;
}
#player-options .option-container .option-entry:hover {
background-color: rgba(20, 20, 20, 0.25);
}
#player-options .option-container .option-entry input[type=checkbox] {
margin-right: 0.25rem;
}
#player-options .option-container .option-entry input[type=number] {
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
}
#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#player-options .option-container .option-entry label {
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
#player-options .randomize-button {
display: flex;
flex-direction: column;
justify-content: center;
height: 22px;
max-width: 30px;
margin: 0 0 0 0.25rem;
font-size: 14px;
border: 1px solid black;
border-radius: 3px;
background-color: #d3d3d3;
user-select: none;
}
#player-options .randomize-button:hover {
background-color: #c0c0c0;
cursor: pointer;
}
#player-options .randomize-button label {
line-height: 22px;
padding-left: 5px;
padding-right: 2px;
margin-right: 4px;
width: 100%;
height: 100%;
min-width: unset;
}
#player-options .randomize-button label:hover {
cursor: pointer;
}
#player-options .randomize-button input[type=checkbox] {
display: none;
}
#player-options .randomize-button:has(input[type=checkbox]:checked) {
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
}
#player-options .randomize-button:has(input[type=checkbox]:checked):hover {
background-color: #eedd27;
}
#player-options .randomize-button[data-tooltip]::after {
left: unset;
right: 0;
}
#player-options label {
display: block;
margin-right: 4px;
cursor: default;
word-break: break-word;
}
#player-options th, #player-options td {
border: none;
padding: 3px;
font-size: 17px;
vertical-align: top;
}
@media all and (max-width: 1024px) {
#player-options {
border-radius: 0;
}
#player-options #meta-options {
flex-direction: column;
justify-content: flex-start;
gap: 6px;
}
#player-options .game-options {
justify-content: flex-start;
flex-wrap: wrap;
}
}
/*# sourceMappingURL=playerOptions.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"}

View File

@@ -0,0 +1,364 @@
@import "../markdown.css";
html{
background-image: url('../../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
overflow-x: hidden;
}
#player-options{
box-sizing: border-box;
max-width: 1024px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
word-break: break-all;
#player-options-header{
h1{
margin-bottom: 0;
padding-bottom: 0;
}
h1:nth-child(2){
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
}
.js-warning-banner{
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
.group-container{
padding: 0;
margin: 0;
h2{
user-select: none;
cursor: unset;
label{
cursor: pointer;
}
}
}
#player-options-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
cursor: pointer;
}
h1{
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 4px #000000;
}
h2{
font-size: 40px;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
h3, h4, h5, h6{
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
&:focus{
border: 1px solid #ffffff;
}
}
select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
text-overflow: ellipsis;
}
.game-options{
display: flex;
flex-direction: row;
.left, .right{
display: grid;
grid-template-columns: 12rem auto;
grid-row-gap: 0.5rem;
grid-auto-rows: min-content;
align-items: start;
min-width: 480px;
width: 50%;
}
}
#meta-options{
display: flex;
justify-content: space-between;
gap: 20px;
padding: 3px;
input, select{
box-sizing: border-box;
width: 200px;
}
}
.left, .right{
flex-grow: 1;
margin-bottom: 0.5rem;
}
.left{
margin-right: 20px;
}
.select-container{
display: flex;
flex-direction: row;
max-width: 270px;
select{
min-width: 200px;
flex-grow: 1;
&:disabled{
background-color: lightgray;
}
}
}
.range-container{
display: flex;
flex-direction: row;
max-width: 270px;
input[type=range]{
flex-grow: 1;
}
.range-value{
min-width: 20px;
margin-left: 0.25rem;
}
}
.named-range-container{
display: flex;
flex-direction: column;
max-width: 270px;
.named-range-wrapper{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
input[type=range]{
flex-grow: 1;
}
}
}
.free-text-container{
display: flex;
flex-direction: column;
max-width: 270px;
input[type=text]{
flex-grow: 1;
}
}
.text-choice-container{
display: flex;
flex-direction: column;
max-width: 270px;
.text-choice-wrapper{
display: flex;
flex-direction: row;
margin-bottom: 0.25rem;
select{
flex-grow: 1;
}
}
}
.option-container{
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 10rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
.option-divider{
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
.option-entry{
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 0.125rem;
margin-top: 0.125rem;
user-select: none;
&:hover{
background-color: rgba(20, 20, 20, 0.25);
}
input[type=checkbox]{
margin-right: 0.25rem;
}
input[type=number]{
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
-webkit-appearance: none;
margin: 0;
}
}
label{
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
}
}
.randomize-button{
display: flex;
flex-direction: column;
justify-content: center;
height: 22px;
max-width: 30px;
margin: 0 0 0 0.25rem;
font-size: 14px;
border: 1px solid black;
border-radius: 3px;
background-color: #d3d3d3;
user-select: none;
&:hover{
background-color: #c0c0c0;
cursor: pointer;
}
label{
line-height: 22px;
padding-left: 5px;
padding-right: 2px;
margin-right: 4px;
width: 100%;
height: 100%;
min-width: unset;
&:hover{
cursor: pointer;
}
}
input[type=checkbox]{
display: none;
}
&:has(input[type=checkbox]:checked){
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
&:hover{
background-color: #eedd27;
}
}
&[data-tooltip]::after{
left: unset;
right: 0;
}
}
label{
display: block;
margin-right: 4px;
cursor: default;
word-break: break-word;
}
th, td{
border: none;
padding: 3px;
font-size: 17px;
vertical-align: top;
}
}
@media all and (max-width: 1024px) {
#player-options {
border-radius: 0;
#meta-options {
flex-direction: column;
justify-content: flex-start;
gap: 6px;
}
.game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
}
}

View File

@@ -8,30 +8,15 @@
cursor: unset;
}
#games h1{
#games h1, #games details summary.h1{
font-size: 60px;
cursor: unset;
}
#games h2{
#games h2, #games details summary.h2{
color: #93dcff;
margin-bottom: 2px;
}
#games .collapse-toggle{
cursor: pointer;
}
#games h2 .collapse-arrow{
font-size: 20px;
display: inline-block; /* make vertical-align work */
padding-bottom: 9px;
vertical-align: middle;
padding-right: 8px;
}
#games p.collapsed{
display: none;
text-transform: none;
}
#games a{

View File

@@ -42,6 +42,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
visibility: visible;
opacity: 1;
word-break: break-word;
}
/** Directional arrow styles */

View File

@@ -1,315 +0,0 @@
html{
background-image: url('../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#weighted-settings #games-wrapper{
width: 100%;
}
#weighted-settings .setting-wrapper{
width: 100%;
margin-bottom: 2rem;
}
#weighted-settings .setting-wrapper .add-option-div{
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
}
#weighted-settings .setting-wrapper .add-option-div button{
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
}
#weighted-settings .setting-wrapper .add-option-div button:active{
margin-bottom: 1px;
}
#weighted-settings p.setting-description{
margin: 0 0 1rem;
}
#weighted-settings p.hint-text{
margin: 0 0 1rem;
font-style: italic;
}
#weighted-settings .jump-link{
color: #ffef00;
cursor: pointer;
text-decoration: underline;
}
#weighted-settings table{
width: 100%;
}
#weighted-settings table th, #weighted-settings table td{
border: none;
}
#weighted-settings table td{
padding: 5px;
}
#weighted-settings table .td-left{
font-family: LexendDeca-Regular, sans-serif;
padding-right: 1rem;
width: 200px;
}
#weighted-settings table .td-middle{
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
#weighted-settings table .td-right{
width: 4rem;
text-align: right;
}
#weighted-settings table .td-delete{
width: 50px;
text-align: right;
}
#weighted-settings table .range-option-delete{
cursor: pointer;
}
#weighted-settings .items-wrapper{
display: flex;
flex-direction: row;
justify-content: space-between;
}
#weighted-settings .items-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .items-wrapper .item-set-wrapper{
width: 24%;
font-weight: bold;
}
#weighted-settings .item-container{
border: 1px solid #ffffff;
border-radius: 2px;
width: 100%;
height: 300px;
overflow-y: auto;
overflow-x: hidden;
margin-top: 0.125rem;
font-weight: normal;
}
#weighted-settings .item-container .item-div{
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings .item-container .item-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .item-container .item-qty-div{
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
display: flex;
flex-direction: column;
justify-content: space-around;
}
#weighted-settings .item-container .item-qty-div input{
min-width: unset;
width: 1.5rem;
text-align: center;
}
#weighted-settings .item-container .item-qty-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .hints-div, #weighted-settings .locations-div{
margin-top: 2rem;
}
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .hints-container, #weighted-settings .locations-container{
display: flex;
flex-direction: row;
justify-content: space-between;
}
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
width: calc(50% - 0.5rem);
font-weight: bold;
}
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
margin-top: 0.25rem;
height: 300px;
font-weight: normal;
}
#weighted-settings #weighted-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#weighted-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#weighted-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#weighted-settings #user-message.visible{
display: block;
cursor: pointer;
}
#weighted-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#weighted-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: none;
text-shadow: 1px 1px 2px #000000;
}
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
text-transform: none;
}
#weighted-settings a{
color: #ffef00;
cursor: pointer;
}
#weighted-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#weighted-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#weighted-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#weighted-settings .game-options, #weighted-settings .rom-options{
display: flex;
flex-direction: column;
}
#weighted-settings .simple-list{
display: flex;
flex-direction: column;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ffffff;
border-radius: 4px;
}
#weighted-settings .simple-list .list-row label{
display: block;
width: calc(100% - 0.5rem);
padding: 0.0625rem 0.25rem;
}
#weighted-settings .simple-list .list-row label:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .simple-list .list-row label input[type=checkbox]{
margin-right: 0.5rem;
}
#weighted-settings .simple-list hr{
width: calc(100% - 2px);
margin: 2px auto;
border-bottom: 1px solid rgb(255 255 255 / 0.6);
}
#weighted-settings .invisible{
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#weighted-settings .game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -0,0 +1,232 @@
html {
background-image: url("../../static/backgrounds/grass.png");
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-options {
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#weighted-options #weighted-options-header h1 {
margin-bottom: 0;
padding-bottom: 0;
}
#weighted-options #weighted-options-header h1:nth-child(2) {
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
#weighted-options .js-warning-banner {
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
#weighted-options .option-wrapper {
width: 100%;
margin-bottom: 2rem;
}
#weighted-options .option-wrapper .add-option-div {
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
}
#weighted-options .option-wrapper .add-option-div button {
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
}
#weighted-options .option-wrapper .add-option-div button:active {
margin-bottom: 1px;
}
#weighted-options p.option-description {
margin: 0 0 1rem;
}
#weighted-options p.hint-text {
margin: 0 0 1rem;
font-style: italic;
}
#weighted-options table {
width: 100%;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
}
#weighted-options table th, #weighted-options table td {
border: none;
}
#weighted-options table td {
padding: 5px;
}
#weighted-options table .td-left {
font-family: LexendDeca-Regular, sans-serif;
padding-right: 1rem;
width: 200px;
}
#weighted-options table .td-middle {
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
#weighted-options table .td-right {
width: 4rem;
text-align: right;
}
#weighted-options table .td-delete {
width: 50px;
text-align: right;
}
#weighted-options table .range-option-delete {
cursor: pointer;
}
#weighted-options #weighted-options-button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#weighted-options #user-message {
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#weighted-options #user-message.visible {
display: block;
cursor: pointer;
}
#weighted-options h1 {
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#weighted-options h2, #weighted-options details summary.h2 {
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: none;
text-shadow: 1px 1px 2px #000000;
}
#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 {
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
text-transform: none;
cursor: unset;
}
#weighted-options h3.option-group-header {
margin-top: 0.75rem;
font-weight: bold;
}
#weighted-options a {
color: #ffef00;
cursor: pointer;
}
#weighted-options input:not([type]) {
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#weighted-options input:not([type]):focus {
border: 1px solid #ffffff;
}
#weighted-options .invisible {
display: none;
}
#weighted-options .unsupported-option {
margin-top: 0.5rem;
}
#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container {
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 15rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
margin-top: 0.5rem;
}
#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider {
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry {
display: flex;
flex-direction: row;
align-items: flex-start;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
user-select: none;
line-height: 1rem;
}
#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover {
background-color: rgba(20, 20, 20, 0.25);
}
#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] {
margin-right: 0.25rem;
}
#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] {
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
}
#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label {
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
.hidden {
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait) {
#weighted-options .game-options {
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label {
display: block;
min-width: 200px;
}
}
/*# sourceMappingURL=weightedOptions.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}

View File

@@ -0,0 +1,274 @@
html{
background-image: url('../../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-options{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
#weighted-options-header{
h1{
margin-bottom: 0;
padding-bottom: 0;
}
h1:nth-child(2){
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
}
.js-warning-banner{
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
.option-wrapper{
width: 100%;
margin-bottom: 2rem;
.add-option-div{
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
button{
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
&:active{
margin-bottom: 1px;
}
}
}
}
p{
&.option-description{
margin: 0 0 1rem;
}
&.hint-text{
margin: 0 0 1rem;
font-style: italic;
};
}
table{
width: 100%;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
th, td{
border: none;
}
td{
padding: 5px;
}
.td-left{
font-family: LexendDeca-Regular, sans-serif;
padding-right: 1rem;
width: 200px;
}
.td-middle{
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
.td-right{
width: 4rem;
text-align: right;
}
.td-delete{
width: 50px;
text-align: right;
}
.range-option-delete{
cursor: pointer;
}
}
#weighted-options-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
&.visible{
display: block;
cursor: pointer;
}
}
h1{
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
h2, details summary.h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: none;
text-shadow: 1px 1px 2px #000000;
}
h3, h4, h5, h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
text-transform: none;
cursor: unset;
}
h3{
&.option-group-header{
margin-top: 0.75rem;
font-weight: bold;
}
}
a{
color: #ffef00;
cursor: pointer;
}
input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
&:focus{
border: 1px solid #ffffff;
}
}
.invisible{
display: none;
}
.unsupported-option{
margin-top: 0.5rem;
}
.set-container, .dict-container, .list-container{
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 15rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
margin-top: 0.5rem;
.divider{
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
.set-entry, .dict-entry, .list-entry{
display: flex;
flex-direction: row;
align-items: flex-start;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
user-select: none;
line-height: 1rem;
&:hover{
background-color: rgba(20, 20, 20, 0.25);
}
input[type=checkbox]{
margin-right: 0.25rem;
}
input[type=number]{
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
-webkit-appearance: none;
margin: 0;
}
}
label{
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
}
}
}
.hidden{
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#weighted-options .game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -1,62 +0,0 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>{{ game }} Options</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-options.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-options.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/'+theme+'Header.html' %}
<div id="player-options" class="markdown" data-game="{{ game }}">
<div id="user-message"></div>
<h1><span id="game-name">Player</span> Options</h1>
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
or download an options file you can use to participate in a MultiWorld.</p>
<p>
A more advanced options configuration for all games can be found on the
<a href="/weighted-options">Weighted options</a> page.
<br />
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
<br />
You may also download the
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
</p>
<div id="meta-options">
<div>
<label for="player-name">
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
</label>
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
</div>
<div>
<label for="game-options-preset">
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
</label>
<select id="game-options-preset">
<option value="__default">Defaults</option>
<option value="__custom" hidden>Custom</option>
</select>
</div>
</div>
<h2>Game Options</h2>
<div id="game-options">
<div id="game-options-left" class="left"></div>
<div id="game-options-right" class="right"></div>
</div>
<div id="player-options-button-row">
<button id="export-options">Export Options</button>
<button id="generate-game">Generate Game</button>
<button id="generate-race">Generate Race</button>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,210 @@
{% macro Toggle(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="select-container">
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
{% if option.default == 1 %}
<option value="false">No</option>
<option value="true" selected>Yes</option>
{% else %}
<option value="false" selected>No</option>
<option value="true">Yes</option>
{% endif %}
</select>
{{ RandomizeButton(option_name, option) }}
</div>
{% endmacro %}
{% macro Choice(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="select-container">
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
{% for id, name in option.name_lookup.items() %}
{% if name != "random" %}
{% if option.default == id %}
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
{% else %}
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
{% endif %}
{% endif %}
{% endfor %}
</select>
{{ RandomizeButton(option_name, option) }}
</div>
{% endmacro %}
{% macro Range(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="range-container">
<input
type="range"
id="{{ option_name }}"
name="{{ option_name }}"
min="{{ option.range_start }}"
max="{{ option.range_end }}"
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
{{ "disabled" if option.default == "random" }}
/>
<span id="{{ option_name }}-value" class="range-value js-required">
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
</span>
{{ RandomizeButton(option_name, option) }}
</div>
{% endmacro %}
{% macro NamedRange(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="named-range-container">
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
{% for key, val in option.special_range_names.items() %}
{% if option.default == val %}
<option value="{{ val }}" selected>{{ key }} ({{ val }})</option>
{% else %}
<option value="{{ val }}">{{ key }} ({{ val }})</option>
{% endif %}
{% endfor %}
<option value="custom" hidden>Custom</option>
</select>
<div class="named-range-wrapper">
<input
type="range"
id="{{ option_name }}"
name="{{ option_name }}"
min="{{ option.range_start }}"
max="{{ option.range_end }}"
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
{{ "disabled" if option.default == "random" }}
/>
<span id="{{ option_name }}-value" class="range-value js-required">
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
</span>
{{ RandomizeButton(option_name, option) }}
</div>
</div>
{% endmacro %}
{% macro FreeText(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="free-text-container">
<input type="text" id="{{ option_name }}" name="{{ option_name }}" value="{{ option.default }}" />
</div>
{% endmacro %}
{% macro TextChoice(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="text-choice-container">
<div class="text-choice-wrapper">
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
{% for id, name in option.name_lookup.items()|sort %}
{% if name != "random" %}
{% if option.default == id %}
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
{% else %}
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
{% endif %}
{% endif %}
{% endfor %}
<option value="custom" hidden>Custom</option>
</select>
{{ RandomizeButton(option_name, option) }}
</div>
<input type="text" id="{{ option_name }}-custom" name="{{ option_name }}-custom" data-option-name="{{ option_name }}" placeholder="Custom value..." />
</div>
{% endmacro %}
{% macro ItemDict(option_name, option, world) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for item_name in world.item_names|sort %}
<div class="option-entry">
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro OptionList(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
<div class="option-entry">
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}" value="{{ key }}" {{ "checked" if key in option.default }} />
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro LocationSet(option_name, option, world) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for group_name in world.location_name_groups.keys()|sort %}
{% if group_name != "Everywhere" %}
<div class="option-entry">
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if grop_name in option.default }} />
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
</div>
{% endif %}
{% endfor %}
{% if world.location_name_groups.keys()|length > 1 %}
<div class="option-divider">&nbsp;</div>
{% endif %}
{% for location_name in world.location_names|sort %}
<div class="option-entry">
<input type="checkbox" id="{{ option_name }}-{{ location_name }}" name="{{ option_name }}" value="{{ location_name }}" {{ "checked" if location_name in option.default }} />
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro ItemSet(option_name, option, world) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for group_name in world.item_name_groups.keys()|sort %}
{% if group_name != "Everything" %}
<div class="option-entry">
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if group_name in option.default }} />
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
</div>
{% endif %}
{% endfor %}
{% if world.item_name_groups.keys()|length > 1 %}
<div class="option-divider">&nbsp;</div>
{% endif %}
{% for item_name in world.item_names|sort %}
<div class="option-entry">
<input type="checkbox" id="{{ option_name }}-{{ item_name }}" name="{{ option_name }}" value="{{ item_name }}" {{ "checked" if item_name in option.default }} />
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro OptionSet(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
<div class="option-entry">
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}" value="{{ key }}" {{ "checked" if key in option.default }} />
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro OptionTitle(option_name, option) %}
<label for="{{ option_name }}">
{{ option.display_name|default(option_name) }}:
<span class="interactive" data-tooltip="{% filter dedent %}{{(option.__doc__ | default("Please document me!"))|escape }}{% endfilter %}">(?)</span>
</label>
{% endmacro %}
{% macro RandomizeButton(option_name, option) %}
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
<label for="random-{{ option_name }}">
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
🎲
</label>
</div>
{% endmacro %}

View File

@@ -0,0 +1,166 @@
{% extends 'pageWrapper.html' %}
{% import 'playerOptions/macros.html' as inputs %}
{% block head %}
<title>{{ world_name }} Options</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerOptions/playerOptions.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerOptions.js") }}"></script>
<noscript>
<style>
.js-required{
display: none;
}
</style>
</noscript>
{% endblock %}
{% block body %}
{% include 'header/'+theme+'Header.html' %}
<div id="player-options" class="markdown" data-game="{{ world_name }}" data-presets="{{ presets }}">
<noscript>
<div class="js-warning-banner">
This page has reduced functionality without JavaScript.
</div>
</noscript>
<div id="user-message">{{ message }}</div>
<div id="player-options-header">
<h1>{{ world_name }}</h1>
<h1>Player Options</h1>
</div>
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
or download an options file you can use to participate in a MultiWorld.</p>
<p>
A more advanced options configuration for all games can be found on the
<a href="weighted-options">Weighted options</a> page.
<br />
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
<br />
You may also download the
<a href="/static/generated/configs/{{ world_name }}.yaml">template file for this game</a>.
</p>
<form id="options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-yaml">
<div id="meta-options">
<div>
<label for="player-name">
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
</label>
<input id="player-name" placeholder="Player" name="name" maxlength="16" />
</div>
<div class="js-required">
<label for="game-options-preset">
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
</label>
<select id="game-options-preset" name="game-options-preset" disabled>
<option value="default">Default</option>
{% for preset_name in world.web.options_presets %}
<option value="{{ preset_name }}">{{ preset_name }}</option>
{% endfor %}
<option value="custom" hidden>Custom</option>
</select>
</div>
</div>
<div id="option-groups">
{% for group_name, group_options in option_groups.items() %}
<details class="group-container" {% if loop.index == 1 %}open{% endif %}>
<summary class="h2">{{ group_name }}</summary>
<div class="game-options">
<div class="left">
{% for option_name, option in group_options.items() %}
{% if loop.index <= (loop.length / 2)|round(0,"ceil") %}
{% if issubclass(option, Options.Toggle) %}
{{ inputs.Toggle(option_name, option) }}
{% elif issubclass(option, Options.TextChoice) %}
{{ inputs.TextChoice(option_name, option) }}
{% elif issubclass(option, Options.Choice) %}
{{ inputs.Choice(option_name, option) }}
{% elif issubclass(option, Options.NamedRange) %}
{{ inputs.NamedRange(option_name, option) }}
{% elif issubclass(option, Options.Range) %}
{{ inputs.Range(option_name, option) }}
{% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
{{ inputs.ItemDict(option_name, option, world) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }}
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
{{ inputs.LocationSet(option_name, option, world) }}
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
{{ inputs.ItemSet(option_name, option, world) }}
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
{{ inputs.OptionSet(option_name, option) }}
{% endif %}
{% endif %}
{% endfor %}
</div>
<div class="right">
{% for option_name, option in group_options.items() %}
{% if loop.index > (loop.length / 2)|round(0,"ceil") %}
{% if issubclass(option, Options.Toggle) %}
{{ inputs.Toggle(option_name, option) }}
{% elif issubclass(option, Options.TextChoice) %}
{{ inputs.TextChoice(option_name, option) }}
{% elif issubclass(option, Options.Choice) %}
{{ inputs.Choice(option_name, option) }}
{% elif issubclass(option, Options.NamedRange) %}
{{ inputs.NamedRange(option_name, option) }}
{% elif issubclass(option, Options.Range) %}
{{ inputs.Range(option_name, option) }}
{% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
{{ inputs.ItemDict(option_name, option, world) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }}
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
{{ inputs.LocationSet(option_name, option, world) }}
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
{{ inputs.ItemSet(option_name, option, world) }}
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
{{ inputs.OptionSet(option_name, option) }}
{% endif %}
{% endif %}
{% endfor %}
</div>
</div>
</details>
{% endfor %}
</div>
<div id="player-options-button-row">
<input type="submit" name="intent-export" value="Export Options" />
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
</div>
</form>
</div>
{% endblock %}

View File

@@ -24,7 +24,6 @@
<li><a href="/games">Supported Games Page</a></li>
<li><a href="/tutorial">Tutorials Page</a></li>
<li><a href="/user-content">User Content</a></li>
<li><a href="/weighted-options">Weighted Options Page</a></li>
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
<li><a href="/glossary/en">Glossary</a></li>
</ul>
@@ -50,8 +49,12 @@
<ul>
{% for game in games | title_sorted %}
{% if game['has_settings'] %}
<li><a href="{{ url_for('player_options', game=game['title']) }}">{{ game['title'] }}</a></li>
{% endif %}
<li>{{ game['title'] }}</li>
<ul>
<li><a href="{{ url_for('player_options', game=game['title']) }}">Player Options</a></li>
<li><a href="{{ url_for('weighted_options', game=game['title']) }}">Weighted Options</a></li>
</ul>
{% endif %}
{% endfor %}
</ul>
</div>

View File

@@ -41,28 +41,28 @@
</div>
{% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
<h2 class="collapse-toggle" data-game="{{ game_name }}">
<span class="collapse-arrow"></span>{{ game_name }}
</h2>
<p class="collapsed">
<details data-game="{{ game_name }}">
<summary class="h2">{{ game_name }}</summary>
{{ world.__doc__ | default("No description provided.", true) }}<br />
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
<a href="{{ url_for("tutorial_landing", _anchor = game_name | urlencode) }}">Setup Guides</a>
{% endif %}
{% if world.web.options_page is string %}
<span class="link-spacer">|</span>
<a href="{{ world.web.options_page }}">Options Page</a>
<a href="{{ world.web.options_page }}">Options Page (External Link)</a>
{% elif world.web.options_page %}
<span class="link-spacer">|</span>
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
<span class="link-spacer">|</span>
<a href="{{ url_for("weighted_options", game=game_name) }}">Advanced Options</a>
{% endif %}
{% if world.web.bug_report_page %}
<span class="link-spacer">|</span>
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
{% endif %}
</p>
</details>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,48 +0,0 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>{{ game }} Options</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-options.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-options.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="weighted-settings" class="markdown" data-game="{{ game }}">
<div id="user-message"></div>
<h1>Weighted Options</h1>
<p>Weighted options allow you to choose how likely a particular option is to be used in game generation.
The higher an option is weighted, the more likely the option will be chosen. Think of them like
entries in a raffle.</p>
<p>Choose the games and options you would like to play with! You may generate a single-player game from
this page, or download an options file you can use to participate in a MultiWorld.</p>
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
page.</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
items if you are playing in a MultiWorld.</label><br />
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
</p>
<div id="game-choice">
<!-- User chooses games by weight -->
</div>
<!-- To be generated and populated per-game with weight > 0 -->
<div id="games-wrapper">
</div>
<div id="weighted-settings-button-row">
<button id="export-options">Export Options</button>
<button id="generate-game">Generate Game</button>
<button id="generate-race">Generate Race</button>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,249 @@
{% macro Toggle(option_name, option) %}
<table>
<tbody>
{{ RangeRow(option_name, option, "No", "false") }}
{{ RangeRow(option_name, option, "Yes", "true") }}
{{ RandomRows(option_name, option) }}
</tbody>
</table>
{% endmacro %}
{% macro DefaultOnToggle(option_name, option) %}
<!-- Toggle handles defaults properly, so we just reuse that -->
{{ Toggle(option_name, option) }}
{% endmacro %}
{% macro Choice(option_name, option) %}
<table>
<tbody>
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
{% endif %}
{% endfor %}
{{ RandomRows(option_name, option) }}
</tbody>
</table>
{% endmacro %}
{% macro Range(option_name, option) %}
<div class="hint-text js-required">
This is a range option.
<br /><br />
Accepted values:<br />
Normal range: {{ option.range_start }} - {{ option.range_end }}
{% if option.special_range_names %}
<br /><br />
The following values has special meaning, and may fall outside the normal range.
<ul>
{% for name, value in option.special_range_names.items() %}
<li>{{ value }}: {{ name }}</li>
{% endfor %}
</ul>
{% endif %}
<div class="add-option-div">
<input type="number" class="range-option-value" data-option="{{ option_name }}" />
<button class="add-range-option-button" data-option="{{ option_name }}">Add</button>
</div>
</div>
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
{% if option.range_start < option.default < option.range_end %}
{{ RangeRow(option_name, option, option.default, option.default, True) }}
{% endif %}
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
{{ RandomRows(option_name, option) }}
</tbody>
</table>
{% endmacro %}
{% macro NamedRange(option_name, option) %}
<!-- Range is able to properly handle NamedDRange options -->
{{ Range(option_name, option) }}
{% endmacro %}
{% macro FreeText(option_name, option) %}
<div class="hint-text">
This option allows custom values only. Please enter your desired values below.
<div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button data-option="{{ option_name }}">Add</button>
</div>
<table>
<tbody>
<!-- This table to be filled by JS -->
</tbody>
</table>
</div>
{% endmacro %}
{% macro TextChoice(option_name, option) %}
<div class="hint-text">
Custom values are also allowed for this option. To create one, enter it into the input box below.
<div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button data-option="{{ option_name }}">Add</button>
</div>
</div>
<table>
<tbody>
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
{% endif %}
{% endfor %}
{{ RandomRows(option_name, option) }}
</tbody>
</table>
{% endmacro %}
{% macro PlandoBosses(option_name, option) %}
<!-- PlandoBosses is handled by its parent, TextChoice -->
{{ TextChoice(option_name, option) }}
{% endmacro %}
{% macro ItemDict(option_name, option, world) %}
<div class="dict-container">
{% for item_name in world.item_names|sort %}
<div class="dict-entry">
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
<input
type="number"
id="{{ option_name }}-{{ item_name }}-qty"
name="{{ option_name }}||{{ item_name }}"
value="0"
/>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro OptionList(option_name, option) %}
<div class="list-container">
{% for key in option.valid_keys|sort %}
<div class="list-entry">
<input
type="checkbox"
id="{{ option_name }}-{{ key }}"
name="{{ option_name }}||{{ key }}"
value="1"
/>
<label for="{{ option_name }}-{{ key }}">
{{ key }}
</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro LocationSet(option_name, option, world) %}
<div class="set-container">
{% for group_name in world.location_name_groups.keys()|sort %}
{% if group_name != "Everywhere" %}
<div class="set-entry">
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if grop_name in option.default }} />
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
</div>
{% endif %}
{% endfor %}
{% if world.location_name_groups.keys()|length > 1 %}
<div class="divider">&nbsp;</div>
{% endif %}
{% for location_name in world.location_names|sort %}
<div class="set-entry">
<input type="checkbox" id="{{ option_name }}-{{ location_name }}" name="{{ option_name }}||{{ location_name }}" value="1" {{ "checked" if location_name in option.default }} />
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro ItemSet(option_name, option, world) %}
<div class="set-container">
{% for group_name in world.item_name_groups.keys()|sort %}
{% if group_name != "Everything" %}
<div class="set-entry">
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if group_name in option.default }} />
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
</div>
{% endif %}
{% endfor %}
{% if world.item_name_groups.keys()|length > 1 %}
<div class="set-divider">&nbsp;</div>
{% endif %}
{% for item_name in world.item_names|sort %}
<div class="set-entry">
<input type="checkbox" id="{{ option_name }}-{{ item_name }}" name="{{ option_name }}||{{ item_name }}" value="1" {{ "checked" if item_name in option.default }} />
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro OptionSet(option_name, option) %}
<div class="set-container">
{% for key in option.valid_keys|sort %}
<div class="set-entry">
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" {{ "checked" if key in option.default }} />
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro OptionTitleTd(option_name, value) %}
<td class="td-left">
<label for="{{ option_name }}||{{ value }}">
{{ option.display_name|default(option_name) }}
</label>
</td>
{% endmacro %}
{% macro RandomRows(option_name, option, extra_column=False) %}
{% for key, value in {"Random": "random", "Random (Low)": "random-low", "Random (Middle)": "random-middle", "Random (High)": "random-high"}.items() %}
{{ RangeRow(option_name, option, key, value) }}
{% endfor %}
{% endmacro %}
{% macro RangeRow(option_name, option, display_value, value, can_delete=False) %}
<tr data-row="{{ option_name }}-{{ value }}-row" data-option-name="{{ option_name }}" data-value="{{ value }}">
<td class="td-left">
<label for="{{ option_name }}||{{ value }}">
{{ display_value }}
</label>
</td>
<td class="td-middle">
<input
type="range"
id="{{ option_name }}||{{ value }}"
name="{{ option_name }}||{{ value }}"
min="0"
max="50"
{% if option.default == value %}
value="25"
{% else %}
value="0"
{% endif %}
/>
</td>
<td class="td-right">
<span id="{{ option_name }}||{{ value }}-value">
{% if option.default == value %}
25
{% else %}
0
{% endif %}
</span>
</td>
{% if can_delete %}
<td>
<span class="range-option-delete js-required" data-target="{{ option_name }}-{{ value }}-row">
</span>
</td>
{% else %}
<td><!-- This td empty on purpose --></td>
{% endif %}
</tr>
{% endmacro %}

View File

@@ -0,0 +1,119 @@
{% extends 'pageWrapper.html' %}
{% import 'weightedOptions/macros.html' as inputs %}
{% block head %}
<title>{{ world_name }} Weighted Options</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedOptions/weightedOptions.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedOptions.js") }}"></script>
<noscript>
<style>
.js-required{
display: none;
}
</style>
</noscript>
{% endblock %}
{% block body %}
{% include 'header/'+theme+'Header.html' %}
<div id="weighted-options" class="markdown" data-game="{{ world_name }}">
<noscript>
<div class="js-warning-banner">
This page has reduced functionality without JavaScript.
</div>
</noscript>
<div id="user-message"></div>
<div id="weighted-options-header">
<h1>{{ world_name }}</h1>
<h1>Weighted Options</h1>
</div>
<form id="weighted-options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-weighted-yaml">
<p>Weighted options allow you to choose how likely a particular option&apos;s value is to be used in game
generation. The higher a value is weighted, the more likely the option will be chosen. Think of them like
entries in a raffle.</p>
<p>Choose the options you would like to play with! You may generate a single-player game from
this page, or download an options file you can use to participate in a MultiWorld.</p>
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
page.</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
items if you are playing in a MultiWorld.</label><br />
<input id="player-name" placeholder="Player Name" name="name" maxlength="16" />
</p>
<div id="{{ world_name }}-container">
{% for group_name, group_options in option_groups.items() %}
<details {% if loop.index == 1 %}open{% endif %}>
<summary class="h2">{{ group_name }}</summary>
{% for option_name, option in group_options.items() %}
<div class="option-wrapper">
<h4>{{ option.display_name|default(option_name) }}</h4>
<div class="option-description">
{{ option.__doc__ }}
</div>
{% if issubclass(option, Options.Toggle) %}
{{ inputs.Toggle(option_name, option) }}
{% elif issubclass(option, Options.DefaultOnToggle) %}
{{ inputs.DefaultOnToggle(option_name, option) }}
{% elif issubclass(option, Options.PlandoBosses) %}
{{ inputs.PlandoBosses(option_name, option) }}
{% elif issubclass(option, Options.TextChoice) %}
{{ inputs.TextChoice(option_name, option) }}
{% elif issubclass(option, Options.Choice) %}
{{ inputs.Choice(option_name, option) }}
{% elif issubclass(option, Options.NamedRange) %}
{{ inputs.NamedRange(option_name, option) }}
{% elif issubclass(option, Options.Range) %}
{{ inputs.Range(option_name, option) }}
{% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
{{ inputs.ItemDict(option_name, option, world) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }}
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
{{ inputs.LocationSet(option_name, option, world) }}
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
{{ inputs.ItemSet(option_name, option, world) }}
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
{{ inputs.OptionSet(option_name, option) }}
{% else %}
<div class="unsupported-option">
This option is not supported. Please edit your .yaml file manually.
</div>
{% endif %}
</div>
{% endfor %}
</details>
{% endfor %}
</div>
<div id="weighted-options-button-row">
<input type="submit" name="intent-export" value="Export Options" />
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
</div>
</form>
</div>
{% endblock %}

View File

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

View File

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

View File

@@ -45,7 +45,10 @@ requires:
{% endmacro %}
{{ game }}:
{%- for option_key, option in options.items() %}
{%- for group_name, group_options in option_groups.items() %}
# {{ group_name }}
{%- for option_key, option in group_options.items() %}
{{ option_key }}:
{%- if option.__doc__ %}
# {{ option.__doc__
@@ -83,3 +86,4 @@ requires:
{%- endif -%}
{{ "\n" }}
{%- endfor %}
{%- endfor %}

View File

@@ -16,6 +16,9 @@
# A Link to the Past
/worlds/alttp/ @Berserker66
# Aquaria
/worlds/aquaria/ @tioui
# ArchipIDLE
/worlds/archipidle/ @LegendaryLinux
@@ -25,6 +28,9 @@
# Blasphemous
/worlds/blasphemous/ @TRPG0
# Bomb Rush Cyberfunk
/worlds/bomb_rush_cyberfunk/ @TRPG0
# Bumper Stickers
/worlds/bumpstik/ @FelicitusNeko
@@ -92,6 +98,9 @@
/worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u
# Mario & Luigi: Superstar Saga
/worlds/mlss/ @jamesbrq
# Meritous
/worlds/meritous/ @FelicitusNeko
@@ -194,6 +203,9 @@
# Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
/worlds/yugioh06/ @rensen
# Zillion
/worlds/zillion/ @beauxq

View File

@@ -85,6 +85,25 @@ class ExampleWorld(World):
options: ExampleGameOptions
```
### Option Groups
Options may be categorized into groups for display on the WebHost. Option groups are displayed alphabetically on the
player-options and weighted-options pages. Options without a group name are categorized into a generic "Game Options"
group.
```python
from worlds.AutoWorld import WebWorld
from Options import OptionGroup
class MyWorldWeb(WebWorld):
option_groups = [
OptionGroup('Color Options', [
Options.ColorblindMode,
Options.FlashReduction,
Options.UIColors,
]),
]
```
### Option Checking
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
@@ -155,10 +174,12 @@ Gives the player starting hints for where the items defined here are.
Gives the player starting hints for the items on locations defined here.
### ExcludeLocations
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
Marks locations given here as `LocationProgressType.Excluded` so that neither progression nor useful items can be
placed on them.
### PriorityLocations
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
the pool.
### ItemLinks
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between

View File

@@ -17,13 +17,14 @@ Then run any of the starting point scripts, like Generate.py, and the included M
required modules and after pressing enter proceed to install everything automatically.
After this, you should be able to run the programs.
* `Launcher.py` gives access to many components, including clients registered in `worlds/LauncherComponents.py`.
* The Launcher button "Generate Template Options" will generate default yamls for all worlds.
* With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive.
* `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally.
* `--log_network` is a command line parameter useful for debugging.
* `WebHost.py` will host the website on your computer.
* You can copy `docs/webhost configuration sample.yaml` to `config.yaml`
to change WebHost options (like the web hosting port number).
* As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`.
## Windows

View File

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

View File

@@ -181,8 +181,7 @@ required, and will prevent progression and useful items from being placed at exc
#### Documenting Locations
Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and
location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra
indentation and single newlines will be collapsed into spaces.
location groups. These descriptions will show up in location-selection options on the options pages.
```python
# locations.py
@@ -236,8 +235,7 @@ Other classifications include:
#### Documenting Items
Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item
groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and
single newlines will be collapsed into spaces.
groups. These descriptions will show up in item-selection options on the options pages.
```python
# items.py

View File

@@ -169,6 +169,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmlss"; ValueData: "{#MyAppName}mlsspatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mlsspatch"; ValueData: "Archipelago Mario & Luigi Superstar Saga Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mlsspatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mlsspatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
@@ -194,6 +199,11 @@ Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archi
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apygo06"; ValueData: "{#MyAppName}ygo06patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Archipelago Yu-Gi-Oh 2006 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import unittest
from Fill import distribute_items_restrictive
from NetUtils import encode
from worlds.AutoWorld import AutoWorldRegister, call_all
from worlds import failed_world_loads
from . import setup_solo_multiworld
@@ -47,3 +48,7 @@ class TestImplemented(unittest.TestCase):
for key, data in multiworld.worlds[1].fill_slot_data().items():
self.assertIsInstance(key, str, "keys in slot data must be a string")
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
def test_no_failed_world_loads(self):
if failed_world_loads:
self.fail(f"The following worlds failed to load: {failed_world_loads}")

View File

@@ -25,6 +25,8 @@ class TestBase(unittest.TestCase):
{"medallions", "stones", "rewards", "logic_bottles"},
"Starcraft 2":
{"Missions", "WoL Missions"},
"Yu-Gi-Oh! 2006":
{"Campaign Boss Beaten"}
}
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game_name, game_name=game_name):

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import hashlib
import logging
import pathlib
import random
from random import Random
import re
import sys
import time
@@ -11,11 +11,13 @@ from dataclasses import make_dataclass
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping,
Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union)
from Options import PerGameCommonOptions
from Options import (
ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints
)
from BaseClasses import CollectionState
if TYPE_CHECKING:
import random
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
from . import GamesPackage
from settings import Group
@@ -118,6 +120,33 @@ class AutoLogicRegister(type):
return new_class
class WebWorldRegister(type):
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> WebWorldRegister:
# don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the
# dev, putting it at the end if they don't define options in it
option_groups: List[OptionGroup] = dct.get("option_groups", [])
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
seen_options = []
item_group_in_list = False
for group in option_groups:
assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined."
if group.name == "Item & Location Options":
group.options.extend(item_and_loc_options)
item_group_in_list = True
else:
for option in group.options:
assert option not in item_and_loc_options, \
f"{option} cannot be moved out of the \"Item & Location Options\" Group"
assert len(group.options) == len(set(group.options)), f"Duplicate options in option group {group.name}"
for option in group.options:
assert option not in seen_options, f"{option} found in two option groups"
seen_options.append(option)
if not item_group_in_list:
option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options))
return super().__new__(mcs, name, bases, dct)
def _timed_call(method: Callable[..., Any], *args: Any,
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
start = time.perf_counter()
@@ -172,7 +201,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
_timed_call(stage_callable, multiworld, *args)
class WebWorld:
class WebWorld(metaclass=WebWorldRegister):
"""Webhost integration"""
options_page: Union[bool, str] = True
@@ -194,6 +223,9 @@ class WebWorld:
options_presets: Dict[str, Dict[str, Any]] = {}
"""A dictionary containing a collection of developer-defined game option presets."""
option_groups: ClassVar[List[OptionGroup]] = []
"""Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
@@ -206,8 +238,8 @@ class World(metaclass=AutoWorldRegister):
game: ClassVar[str]
"""name the game"""
topology_present: ClassVar[bool] = False
"""indicate if world type has any meaningful layout/pathing"""
topology_present: bool = False
"""indicate if this world has any meaningful layout/pathing"""
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
"""gets automatically populated with all item and item group names"""
@@ -283,7 +315,7 @@ class World(metaclass=AutoWorldRegister):
location_names: ClassVar[Set[str]]
"""set of all potential location names"""
random: random.Random
random: Random
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
settings_key: ClassVar[str]
@@ -300,7 +332,7 @@ class World(metaclass=AutoWorldRegister):
assert multiworld is not None
self.multiworld = multiworld
self.player = player
self.random = random.Random(multiworld.random.getrandbits(64))
self.random = Random(multiworld.random.getrandbits(64))
multiworld.per_slot_randoms[player] = self.random
def __getattr__(self, item: str) -> Any:
@@ -504,6 +536,10 @@ class World(metaclass=AutoWorldRegister):
def get_region(self, region_name: str) -> "Region":
return self.multiworld.get_region(region_name, self.player)
@property
def player_name(self) -> str:
return self.multiworld.get_player_name(self.player)
@classmethod
def get_data_package_data(cls) -> "GamesPackage":
sorted_item_name_groups = {

View File

@@ -241,4 +241,4 @@ adventure_option_definitions: Dict[str, type(Option)] = {
"difficulty_switch_b": DifficultySwitchB,
"start_castle": StartCastle,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,7 +64,8 @@ configuración personal y descargar un fichero "YAML".
### Configuración YAML avanzada
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
["Weighted settings"](/games/A Link to the Past/weighted-options),
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
elegidos sobre otros de la misma.

View File

@@ -66,9 +66,10 @@ paramètres personnels et de les exporter vers un fichier YAML.
### Configuration avancée du fichier YAML
Une version plus avancée du fichier YAML peut être créée en utilisant la page
des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page
a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir
quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à
trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
autres disponibles dans une même catégorie.
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

210
worlds/aquaria/Items.py Normal file
View File

@@ -0,0 +1,210 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Manage items in the Aquaria game multiworld randomizer
"""
from typing import Optional
from enum import Enum
from BaseClasses import Item, ItemClassification
class ItemType(Enum):
"""
Used to indicate to the multi-world if an item is usefull or not
"""
NORMAL = 0
PROGRESSION = 1
JUNK = 2
class ItemGroup(Enum):
"""
Used to group items
"""
COLLECTIBLE = 0
INGREDIENT = 1
RECIPE = 2
HEALTH = 3
UTILITY = 4
SONG = 5
TURTLE = 6
class AquariaItem(Item):
"""
A single item in the Aquaria game.
"""
game: str = "Aquaria"
"""The name of the game"""
def __init__(self, name: str, classification: ItemClassification,
code: Optional[int], player: int):
"""
Initialisation of the Item
:param name: The name of the item
:param classification: If the item is usefull or not
:param code: The ID of the item (if None, it is an event)
:param player: The ID of the player in the multiworld
"""
super().__init__(name, classification, code, player)
class ItemData:
"""
Data of an item.
"""
id:int
count:int
type:ItemType
group:ItemGroup
def __init__(self, id:int, count:int, type:ItemType, group:ItemGroup):
"""
Initialisation of the item data
@param id: The item ID
@param count: the number of items in the pool
@param type: the importance type of the item
@param group: the usage of the item in the game
"""
self.id = id
self.count = count
self.type = type
self.group = group
"""Information data for every (not event) item."""
item_table = {
# name: ID, Nb, Item Type, Item Group
"Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
"Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
"Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
"Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
"Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
"Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
"Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
"Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
"Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
"Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
"Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
"Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
"Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
"Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
"Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
"Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
"Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
"Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
"Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
"Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
"Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
"Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
"Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
"Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
"Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
"Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
"Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
"Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
"Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
"Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
"Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
"Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
"Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
"Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
"Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
"Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
"Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
"Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
"Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
"Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
"Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
"Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
"Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
"Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
"Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
"Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
"Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
"Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
"Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
"Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
"Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
"Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
"Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
"Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
"Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
"Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
"Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
"Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
"Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
"Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
"Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
"Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
"Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
"Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
"Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
"Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
"Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
"Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
"Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
"Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
"Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
"Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
"Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
"Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
"Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
"Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
"Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
"Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
"Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
"Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
"Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
"Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
"Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
"Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
"Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
"Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
"Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
"Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
"Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
"Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
"Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
"Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
"Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
"Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
"Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
"Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
"Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
"Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
"Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
"Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
"Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
"Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
"Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
"Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
"Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
"Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
"Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
"Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
"Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
"Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
"Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
"Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
"Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
"Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
"Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
"Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
"Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
"Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
"Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
"Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
"Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
ItemGroup.TURTLE), # transport_openwater03
"Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
"Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
"Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
"Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
"Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
"Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
}

574
worlds/aquaria/Locations.py Normal file
View File

@@ -0,0 +1,574 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Manage locations in the Aquaria game multiworld randomizer
"""
from BaseClasses import Location
class AquariaLocation(Location):
"""
A location in the game.
"""
game: str = "Aquaria"
"""The name of the game"""
def __init__(self, player: int, name="", code=None, parent=None) -> None:
"""
Initialisation of the object
:param player: the ID of the player
:param name: the name of the location
:param code: the ID (or address) of the location (Event if None)
:param parent: the Region that this location belongs to
"""
super(AquariaLocation, self).__init__(player, name, code, parent)
self.event = code is None
class AquariaLocations:
locations_verse_cave_r = {
"Verse cave, bulb in the skeleton room": 698107,
"Verse cave, bulb in the path left of the skeleton room": 698108,
"Verse cave right area, Big Seed": 698175,
}
locations_verse_cave_l = {
"Verse cave, the Naija hint about here shield ability": 698200,
"Verse cave left area, bulb in the center part": 698021,
"Verse cave left area, bulb in the right part": 698022,
"Verse cave left area, bulb under the rock at the end of the path": 698023,
}
locations_home_water = {
"Home water, bulb below the grouper fish": 698058,
"Home water, bulb in the path bellow Nautilus Prime": 698059,
"Home water, bulb in the little room above the grouper fish": 698060,
"Home water, bulb in the end of the left path from the verse cave": 698061,
"Home water, bulb in the top left path": 698062,
"Home water, bulb in the bottom left room": 698063,
"Home water, bulb close to the Naija's home": 698064,
"Home water, bulb under the rock in the left path from the verse cave": 698065,
}
locations_home_water_nautilus = {
"Home water, Nautilus Egg": 698194,
}
locations_home_water_transturtle = {
"Home water, Transturtle": 698213,
}
locations_naija_home = {
"Naija's home, bulb after the energy door": 698119,
"Naija's home, bulb under the rock at the right of the main path": 698120,
}
locations_song_cave = {
"Song cave, Erulian spirit": 698206,
"Song cave, bulb in the top left part": 698071,
"Song cave, bulb in the big anemone room": 698072,
"Song cave, bulb in the path to the singing statues": 698073,
"Song cave, bulb under the rock in the path to the singing statues": 698074,
"Song cave, bulb under the rock close to the song door": 698075,
"Song cave, Verse egg": 698160,
"Song cave, Jelly beacon": 698178,
"Song cave, Anemone seed": 698162,
}
locations_energy_temple_1 = {
"Energy temple first area, beating the energy statue": 698205,
"Energy temple first area, bulb in the bottom room blocked by a rock": 698027,
}
locations_energy_temple_idol = {
"Energy temple first area, Energy Idol": 698170,
}
locations_energy_temple_2 = {
"Energy temple second area, bulb under the rock": 698028,
}
locations_energy_temple_altar = {
"Energy temple bottom entrance, Krotite armor": 698163,
}
locations_energy_temple_3 = {
"Energy temple third area, bulb in the bottom path": 698029,
}
locations_energy_temple_boss = {
"Energy temple boss area, Fallen god tooth": 698169,
}
locations_energy_temple_blaster_room = {
"Energy temple blaster room, Blaster egg": 698195,
}
locations_openwater_tl = {
"Open water top left area, bulb under the rock in the right path": 698001,
"Open water top left area, bulb under the rock in the left path": 698002,
"Open water top left area, bulb to the right of the save cristal": 698003,
}
locations_openwater_tr = {
"Open water top right area, bulb in the small path before Mithalas": 698004,
"Open water top right area, bulb in the path from the left entrance": 698005,
"Open water top right area, bulb in the clearing close to the bottom exit": 698006,
"Open water top right area, bulb in the big clearing close to the save cristal": 698007,
"Open water top right area, bulb in the big clearing to the top exit": 698008,
"Open water top right area, first urn in the Mithalas exit": 698148,
"Open water top right area, second urn in the Mithalas exit": 698149,
"Open water top right area, third urn in the Mithalas exit": 698150,
}
locations_openwater_tr_turtle = {
"Open water top right area, bulb in the turtle room": 698009,
"Open water top right area, Transturtle": 698211,
}
locations_openwater_bl = {
"Open water bottom left area, bulb behind the chomper fish": 698011,
"Open water bottom left area, bulb inside the downest fish pass": 698010,
}
locations_skeleton_path = {
"Open water skeleton path, bulb close to the right exit": 698012,
"Open water skeleton path, bulb behind the chomper fish": 698013,
}
locations_skeleton_path_sc = {
"Open water skeleton path, King skull": 698177,
}
locations_arnassi = {
"Arnassi Ruins, bulb in the right part": 698014,
"Arnassi Ruins, bulb in the left part": 698015,
"Arnassi Ruins, bulb in the center part": 698016,
"Arnassi ruins, Song plant spore on the top of the ruins": 698179,
"Arnassi ruins, Arnassi Armor": 698191,
}
locations_arnassi_path = {
"Arnassi Ruins, Arnassi statue": 698164,
"Arnassi Ruins, Transturtle": 698217,
}
locations_arnassi_crab_boss = {
"Arnassi ruins, Crab armor": 698187,
}
locations_simon = {
"Kelp forest, beating Simon says": 698156,
"Simon says area, Transturtle": 698216,
}
locations_mithalas_city = {
"Mithalas city, first bulb in the left city part": 698030,
"Mithalas city, second bulb in the left city part": 698035,
"Mithalas city, bulb in the right part": 698031,
"Mithalas city, bulb at the top of the city": 698033,
"Mithalas city, first bulb in a broken home": 698034,
"Mithalas city, second bulb in a broken home": 698041,
"Mithalas city, bulb in the bottom left part": 698037,
"Mithalas city, first bulb in one of the homes": 698038,
"Mithalas city, second bulb in one of the homes": 698039,
"Mithalas city, first urn in one of the homes": 698123,
"Mithalas city, second urn in one of the homes": 698124,
"Mithalas city, first urn in the city reserve": 698125,
"Mithalas city, second urn in the city reserve": 698126,
"Mithalas city, third urn in the city reserve": 698127,
}
locations_mithalas_city_top_path = {
"Mithalas city, first bulb at the end of the top path": 698032,
"Mithalas city, second bulb at the end of the top path": 698040,
"Mithalas city, bulb in the top path": 698036,
"Mithalas city, Mithalas pot": 698174,
"Mithalas city, urn in the cathedral flower tube entrance": 698128,
}
locations_mithalas_city_fishpass = {
"Mithalas city, Doll": 698173,
"Mithalas city, urn inside a home fish pass": 698129,
}
locations_cathedral_l = {
"Mithalas city castle, bulb in the flesh hole": 698042,
"Mithalas city castle, Blue banner": 698165,
"Mithalas city castle, urn in the bedroom": 698130,
"Mithalas city castle, first urn of the single lamp path": 698131,
"Mithalas city castle, second urn of the single lamp path": 698132,
"Mithalas city castle, urn in the bottom room": 698133,
"Mithalas city castle, first urn on the entrance path": 698134,
"Mithalas city castle, second urn on the entrance path": 698135,
}
locations_cathedral_l_tube = {
"Mithalas castle, beating the priests": 698208,
}
locations_cathedral_l_sc = {
"Mithalas city castle, Trident head": 698183,
}
locations_cathedral_r = {
"Mithalas cathedral, first urn in the top right room": 698136,
"Mithalas cathedral, second urn in the top right room": 698137,
"Mithalas cathedral, third urn in the top right room": 698138,
"Mithalas cathedral, urn in the flesh room with fleas": 698139,
"Mithalas cathedral, first urn in the bottom right path": 698140,
"Mithalas cathedral, second urn in the bottom right path": 698141,
"Mithalas cathedral, urn behind the flesh vein": 698142,
"Mithalas cathedral, urn in the top left eyes boss room": 698143,
"Mithalas cathedral, first urn in the path behind the flesh vein": 698144,
"Mithalas cathedral, second urn in the path behind the flesh vein": 698145,
"Mithalas cathedral, third urn in the path behind the flesh vein": 698146,
"Mithalas cathedral, one of the urns in the top right room": 698147,
"Mithalas cathedral, Mithalan Dress": 698189,
"Mithalas cathedral right area, urn bellow the left entrance": 698198,
}
locations_cathedral_underground = {
"Cathedral underground, bulb in the center part": 698113,
"Cathedral underground, first bulb in the top left part": 698114,
"Cathedral underground, second bulb in the top left part": 698115,
"Cathedral underground, third bulb in the top left part": 698116,
"Cathedral underground, bulb close to the save cristal": 698117,
"Cathedral underground, bulb in the bottom right path": 698118,
}
locations_cathedral_boss = {
"Cathedral boss area, beating Mithalan God": 698202,
}
locations_forest_tl = {
"Kelp Forest top left area, bulb in the bottom left clearing": 698044,
"Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
"Kelp Forest top left area, bulb in the top left clearing": 698046,
"Kelp Forest top left, Jelly Egg": 698185,
}
locations_forest_tl_fp = {
"Kelp Forest top left area, bulb close to the Verse egg": 698047,
"Kelp forest top left area, Verse egg": 698158,
}
locations_forest_tr = {
"Kelp Forest top right area, bulb under the rock in the right path": 698048,
"Kelp Forest top right area, bulb at the left of the center clearing": 698049,
"Kelp Forest top right area, bulb in the left path's big room": 698051,
"Kelp Forest top right area, bulb in the left path's small room": 698052,
"Kelp Forest top right area, bulb at the top of the center clearing": 698053,
"Kelp forest top right area, Black pearl": 698167,
}
locations_forest_tr_fp = {
"Kelp Forest top right area, bulb in the top fish pass": 698050,
}
locations_forest_bl = {
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
"Kelp forest bottom left area, Walker baby": 698186,
"Kelp Forest bottom left area, Transturtle": 698212,
}
locations_forest_br = {
"Kelp forest bottom right area, Odd Container": 698168,
}
locations_forest_boss = {
"Kelp forest boss area, beating Drunian God": 698204,
}
locations_forest_boss_entrance = {
"Kelp Forest boss room, bulb at the bottom of the area": 698055,
}
locations_forest_fish_cave = {
"Kelp Forest bottom left area, Fish cave puzzle": 698207,
}
locations_forest_sprite_cave = {
"Kelp Forest sprite cave, bulb inside the fish pass": 698056,
}
locations_forest_sprite_cave_tube = {
"Kelp Forest sprite cave, bulb in the second room": 698057,
"Kelp Forest Sprite Cave, Seed bag": 698176,
}
locations_mermog_cave = {
"Mermog cave, bulb in the left part of the cave": 698121,
}
locations_mermog_boss = {
"Mermog cave, Piranha Egg": 698197,
}
locations_veil_tl = {
"The veil top left area, In the Li cave": 698199,
"The veil top left area, bulb under the rock in the top right path": 698078,
"The veil top left area, bulb hidden behind the blocking rock": 698076,
"The veil top left area, Transturtle": 698209,
}
locations_veil_tl_fp = {
"The veil top left area, bulb inside the fish pass": 698077,
}
locations_turtle_cave = {
"Turtle cave, Turtle Egg": 698184,
}
locations_turtle_cave_bubble = {
"Turtle cave, bulb in bubble cliff": 698000,
"Turtle cave, Urchin costume": 698193,
}
locations_veil_tr_r = {
"The veil top right area, bulb in the middle of the wall jump cliff": 698079,
"The veil top right area, golden starfish at the bottom right of the bottom path": 698180,
}
locations_veil_tr_l = {
"The veil top right area, bulb in the top of the water fall": 698080,
"The veil top right area, Transturtle": 698210,
}
locations_veil_bl = {
"The veil bottom area, bulb in the left path": 698082,
}
locations_veil_b_sc = {
"The veil bottom area, bulb in the spirit path": 698081,
}
locations_veil_bl_fp = {
"The veil bottom area, Verse egg": 698157,
}
locations_veil_br = {
"The veil bottom area, Stone Head": 698181,
}
locations_octo_cave_t = {
"Octopus cave, Dumbo Egg": 698196,
}
locations_octo_cave_b = {
"Octopus cave, bulb in the path below the octopus cave path": 698122,
}
locations_sun_temple_l = {
"Sun temple, bulb in the top left part": 698094,
"Sun temple, bulb in the top right part": 698095,
"Sun temple, bulb at the top of the high dark room": 698096,
"Sun temple, Golden Gear": 698171,
}
locations_sun_temple_r = {
"Sun temple, first bulb of the temple": 698091,
"Sun temple, bulb on the left part": 698092,
"Sun temple, bulb in the hidden room of the right part": 698093,
"Sun temple, Sun key": 698182,
}
locations_sun_temple_boss_path = {
"Sun Worm path, first path bulb": 698017,
"Sun Worm path, second path bulb": 698018,
"Sun Worm path, first cliff bulb": 698019,
"Sun Worm path, second cliff bulb": 698020,
}
locations_sun_temple_boss = {
"Sun temple boss area, beating Sun God": 698203,
}
locations_abyss_l = {
"Abyss left area, bulb in hidden path room": 698024,
"Abyss left area, bulb in the right part": 698025,
"Abyss left area, Glowing seed": 698166,
"Abyss left area, Glowing Plant": 698172,
}
locations_abyss_lb = {
"Abyss left area, bulb in the bottom fish pass": 698026,
}
locations_abyss_r = {
"Abyss right area, bulb behind the rock in the whale room": 698109,
"Abyss right area, bulb in the middle path": 698110,
"Abyss right area, bulb behind the rock in the middle path": 698111,
"Abyss right area, bulb in the left green room": 698112,
"Abyss right area, Transturtle": 698214,
}
locations_ice_cave = {
"Ice cave, bulb in the room to the right": 698083,
"Ice cave, First bulbs in the top exit room": 698084,
"Ice cave, Second bulbs in the top exit room": 698085,
"Ice cave, third bulbs in the top exit room": 698086,
"Ice cave, bulb in the left room": 698087,
}
locations_bubble_cave = {
"Bubble cave, bulb in the left cave wall": 698089,
"Bubble cave, bulb in the right cave wall (behind the ice cristal)": 698090,
}
locations_bubble_cave_boss = {
"Bubble cave, Verse egg": 698161,
}
locations_king_jellyfish_cave = {
"King Jellyfish cave, bulb in the right path from King Jelly": 698088,
"King Jellyfish cave, Jellyfish Costume": 698188,
}
locations_whale = {
"The whale, Verse egg": 698159,
}
locations_sunken_city_r = {
"Sunken city right area, crate close to the save cristal": 698154,
"Sunken city right area, crate in the left bottom room": 698155,
}
locations_sunken_city_l = {
"Sunken city left area, crate in the little pipe room": 698151,
"Sunken city left area, crate close to the save cristal": 698152,
"Sunken city left area, crate before the bedroom": 698153,
}
locations_sunken_city_l_bedroom = {
"Sunken city left area, Girl Costume": 698192,
}
locations_sunken_city_boss = {
"Sunken city, bulb on the top of the boss area (boiler room)": 698043,
}
locations_body_c = {
"The body center area, breaking li cage": 698201,
"The body main area, bulb on the main path blocking tube": 698097,
}
locations_body_l = {
"The body left area, first bulb in the top face room": 698066,
"The body left area, second bulb in the top face room": 698069,
"The body left area, bulb bellow the water stream": 698067,
"The body left area, bulb in the top path to the top face room": 698068,
"The body left area, bulb in the bottom face room": 698070,
}
locations_body_rt = {
"The body right area, bulb in the top face room": 698100,
}
locations_body_rb = {
"The body right area, bulb in the top path to the bottom face room": 698098,
"The body right area, bulb in the bottom face room": 698099,
}
locations_body_b = {
"The body bottom area, bulb in the Jelly Zap room": 698101,
"The body bottom area, bulb in the nautilus room": 698102,
"The body bottom area, Mutant Costume": 698190,
}
locations_final_boss_tube = {
"Final boss area, first bulb in the turtle room": 698103,
"Final boss area, second bulbs in the turtle room": 698104,
"Final boss area, third bulbs in the turtle room": 698105,
"Final boss area, Transturtle": 698215,
}
locations_final_boss = {
"Final boss area, bulb in the boss third form room": 698106,
}
location_table = {
**AquariaLocations.locations_openwater_tl,
**AquariaLocations.locations_openwater_tr,
**AquariaLocations.locations_openwater_tr_turtle,
**AquariaLocations.locations_openwater_bl,
**AquariaLocations.locations_skeleton_path,
**AquariaLocations.locations_skeleton_path_sc,
**AquariaLocations.locations_arnassi,
**AquariaLocations.locations_arnassi_path,
**AquariaLocations.locations_arnassi_crab_boss,
**AquariaLocations.locations_sun_temple_l,
**AquariaLocations.locations_sun_temple_r,
**AquariaLocations.locations_sun_temple_boss_path,
**AquariaLocations.locations_sun_temple_boss,
**AquariaLocations.locations_verse_cave_r,
**AquariaLocations.locations_verse_cave_l,
**AquariaLocations.locations_abyss_l,
**AquariaLocations.locations_abyss_lb,
**AquariaLocations.locations_abyss_r,
**AquariaLocations.locations_energy_temple_1,
**AquariaLocations.locations_energy_temple_2,
**AquariaLocations.locations_energy_temple_3,
**AquariaLocations.locations_energy_temple_boss,
**AquariaLocations.locations_energy_temple_blaster_room,
**AquariaLocations.locations_energy_temple_altar,
**AquariaLocations.locations_energy_temple_idol,
**AquariaLocations.locations_mithalas_city,
**AquariaLocations.locations_mithalas_city_top_path,
**AquariaLocations.locations_mithalas_city_fishpass,
**AquariaLocations.locations_cathedral_l,
**AquariaLocations.locations_cathedral_l_tube,
**AquariaLocations.locations_cathedral_l_sc,
**AquariaLocations.locations_cathedral_r,
**AquariaLocations.locations_cathedral_underground,
**AquariaLocations.locations_cathedral_boss,
**AquariaLocations.locations_forest_tl,
**AquariaLocations.locations_forest_tl_fp,
**AquariaLocations.locations_forest_tr,
**AquariaLocations.locations_forest_tr_fp,
**AquariaLocations.locations_forest_bl,
**AquariaLocations.locations_forest_br,
**AquariaLocations.locations_forest_boss,
**AquariaLocations.locations_forest_boss_entrance,
**AquariaLocations.locations_forest_sprite_cave,
**AquariaLocations.locations_forest_sprite_cave_tube,
**AquariaLocations.locations_forest_fish_cave,
**AquariaLocations.locations_home_water,
**AquariaLocations.locations_home_water_transturtle,
**AquariaLocations.locations_home_water_nautilus,
**AquariaLocations.locations_body_l,
**AquariaLocations.locations_body_rt,
**AquariaLocations.locations_body_rb,
**AquariaLocations.locations_body_c,
**AquariaLocations.locations_body_b,
**AquariaLocations.locations_final_boss_tube,
**AquariaLocations.locations_final_boss,
**AquariaLocations.locations_song_cave,
**AquariaLocations.locations_veil_tl,
**AquariaLocations.locations_veil_tl_fp,
**AquariaLocations.locations_turtle_cave,
**AquariaLocations.locations_turtle_cave_bubble,
**AquariaLocations.locations_veil_tr_r,
**AquariaLocations.locations_veil_tr_l,
**AquariaLocations.locations_veil_bl,
**AquariaLocations.locations_veil_b_sc,
**AquariaLocations.locations_veil_bl_fp,
**AquariaLocations.locations_veil_br,
**AquariaLocations.locations_ice_cave,
**AquariaLocations.locations_king_jellyfish_cave,
**AquariaLocations.locations_bubble_cave,
**AquariaLocations.locations_bubble_cave_boss,
**AquariaLocations.locations_naija_home,
**AquariaLocations.locations_mermog_cave,
**AquariaLocations.locations_mermog_boss,
**AquariaLocations.locations_octo_cave_t,
**AquariaLocations.locations_octo_cave_b,
**AquariaLocations.locations_sunken_city_l,
**AquariaLocations.locations_sunken_city_r,
**AquariaLocations.locations_sunken_city_boss,
**AquariaLocations.locations_sunken_city_l_bedroom,
**AquariaLocations.locations_simon,
**AquariaLocations.locations_whale,
}

145
worlds/aquaria/Options.py Normal file
View File

@@ -0,0 +1,145 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Manage options in the Aquaria game multiworld randomizer
"""
from dataclasses import dataclass
from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
class IngredientRandomizer(Choice):
"""
Randomize Ingredients. Select if the simple ingredients (that does not have
a recipe) should be randomized. If 'common_ingredients' is selected, the
randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
"""
display_name = "Randomize Ingredients"
option_off = 0
option_common_ingredients = 1
option_all_ingredients = 2
default = 0
class DishRandomizer(Toggle):
"""Randomize the drop of Dishes (Ingredients with recipe)."""
display_name = "Dish Randomizer"
class TurtleRandomizer(Choice):
"""Randomize the transportation turtle."""
display_name = "Turtle Randomizer"
option_no_turtle_randomization = 0
option_randomize_all_turtle = 1
option_randomize_turtle_other_than_the_final_one = 2
default = 2
class EarlyEnergyForm(DefaultOnToggle):
"""
Force the Energy Form to be in a location before leaving the areas around the Home Water.
"""
display_name = "Early Energy Form"
class AquarianTranslation(Toggle):
"""Translate to English the Aquarian scripture in the game."""
display_name = "Translate Aquarian"
class BigBossesToBeat(Range):
"""
A number of big bosses to beat before having access to the creator (the final boss). The big bosses are
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
"""
display_name = "Big bosses to beat"
range_start = 0
range_end = 5
default = 0
class MiniBossesToBeat(Range):
"""
A number of Minibosses to beat before having access to the creator (the final boss). Mini bosses are
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
"Mantis Shrimp Prime" and "King Jellyfish God Prime". Note that the Energy statue and Simon says are not
mini bosses.
"""
display_name = "Mini bosses to beat"
range_start = 0
range_end = 8
default = 0
class Objective(Choice):
"""
The game objective can be only to kill the creator or to kill the creator
and having obtained the three every secret memories
"""
display_name = "Objective"
option_kill_the_creator = 0
option_obtain_secrets_and_kill_the_creator = 1
default = 0
class SkipFirstVision(Toggle):
"""
The first vision in the game; where Naija transform to Energy Form and get fload by enemy; is quite cool but
can be quite long when you already know what is going on. This option can be used to skip this vision.
"""
display_name = "Skip first Naija's vision"
class NoProgressionHardOrHiddenLocation(Toggle):
"""
Make sure that there is no progression items at hard to get or hard to find locations.
Those locations that will be very High location (that need beast form, soup and skill to get), every
location in the bubble cave, locations that need you to cross a false wall without any indication, Arnassi
race, bosses and mini-bosses. Usefull for those that want a casual run.
"""
display_name = "No progression in hard or hidden locations"
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
"""
Make sure that the sun form or the dumbo pet can be aquired before getting to dark places. Be aware that navigating
in dark place without light is extremely difficult.
"""
display_name = "Light needed to get to dark places"
class BindSongNeededToGetUnderRockBulb(Toggle):
"""
Make sure that the bind song can be aquired before having to obtain sing bulb under rocks.
"""
display_name = "Bind song needed to get sing bulbs under rocks"
class UnconfineHomeWater(Choice):
"""
Open the way out of Home water area so that Naija can go to open water and beyond without the bind song.
"""
display_name = "Unconfine Home Water Area"
option_off = 0
option_via_energy_door = 1
option_via_transturtle = 2
option_via_both = 3
default = 0
@dataclass
class AquariaOptions(PerGameCommonOptions):
"""
Every option in the Aquaria randomizer
"""
start_inventory_from_pool: StartInventoryPool
objective: Objective
mini_bosses_to_beat: MiniBossesToBeat
big_bosses_to_beat: BigBossesToBeat
turtle_randomizer: TurtleRandomizer
early_energy_form: EarlyEnergyForm
light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
unconfine_home_water: UnconfineHomeWater
no_progression_hard_or_hidden_locations: NoProgressionHardOrHiddenLocation
ingredient_randomizer: IngredientRandomizer
dish_randomizer: DishRandomizer
aquarian_translation: AquarianTranslation
skip_first_vision: SkipFirstVision
death_link: DeathLink

1401
worlds/aquaria/Regions.py Executable file

File diff suppressed because it is too large Load Diff

218
worlds/aquaria/__init__.py Normal file
View File

@@ -0,0 +1,218 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Main module for Aquaria game multiworld randomizer
"""
from typing import List, Dict, ClassVar, Any
from ..AutoWorld import World, WebWorld
from BaseClasses import Tutorial, MultiWorld, ItemClassification
from .Items import item_table, AquariaItem, ItemType, ItemGroup
from .Locations import location_table
from .Options import AquariaOptions
from .Regions import AquariaRegions
class AquariaWeb(WebWorld):
"""
Class used to generate the Aquaria Game Web pages (setup, tutorial, etc.)
"""
theme = "ocean"
bug_report_page = "https://github.com/tioui/Aquaria_Randomizer/issues"
setup = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Aquaria for MultiWorld.",
"English",
"setup_en.md",
"setup/en",
["Tioui"]
)
setup_fr = Tutorial(
"Guide de configuration Multimonde",
"Un guide pour configurer Aquaria MultiWorld",
"Français",
"setup_fr.md",
"setup/fr",
["Tioui"]
)
tutorials = [setup, setup_fr]
class AquariaWorld(World):
"""
Aquaria is a side-scrolling action-adventure game. It follows Naija, an
aquatic humanoid woman, as she explores the underwater world of Aquaria.
Along her journey, she learns about the history of the world she inhabits
as well as her own past. The gameplay focuses on a combination of swimming,
singing, and combat, through which Naija can interact with the world. Her
songs can move items, affect plants and animals, and change her physical
appearance into other forms that have different abilities, like firing
projectiles at hostile creatures, or passing through barriers inaccessible
to her in her natural form.
From: https://en.wikipedia.org/wiki/Aquaria_(video_game)
"""
game: str = "Aquaria"
"The name of the game"
topology_present = True
"show path to required location checks in spoiler"
web: WebWorld = AquariaWeb()
"The web page generation informations"
item_name_to_id: ClassVar[Dict[str, int]] =\
{name: data.id for name, data in item_table.items()}
"The name and associated ID of each item of the world"
item_name_groups = {
"Damage": {"Energy form", "Nature form", "Beast form",
"Li and Li song", "Baby nautilus", "Baby piranha",
"Baby blaster"},
"Light": {"Sun form", "Baby dumbo"}
}
"""Grouping item make it easier to find them"""
location_name_to_id = location_table
"The name and associated ID of each location of the world"
base_id = 698000
"The starting ID of the items and locations of the world"
ingredients_substitution: List[int]
"Used to randomize ingredient drop"
options_dataclass = AquariaOptions
"Used to manage world options"
options: AquariaOptions
"Every options of the world"
regions: AquariaRegions
"Used to manage Regions"
exclude: List[str]
def __init__(self, multiworld: MultiWorld, player: int):
"""Initialisation of the Aquaria World"""
super(AquariaWorld, self).__init__(multiworld, player)
self.regions = AquariaRegions(multiworld, player)
self.ingredients_substitution = []
self.exclude = []
def create_regions(self) -> None:
"""
Create every Region in `regions`
"""
self.regions.add_regions_to_world()
self.regions.connect_regions()
self.regions.add_event_locations()
def create_item(self, name: str) -> AquariaItem:
"""
Create an AquariaItem using `name' as item name.
"""
result: AquariaItem
try:
data = item_table[name]
classification: ItemClassification = ItemClassification.useful
if data.type == ItemType.JUNK:
classification = ItemClassification.filler
elif data.type == ItemType.PROGRESSION:
classification = ItemClassification.progression
result = AquariaItem(name, classification, data.id, self.player)
except BaseException:
raise Exception('The item ' + name + ' is not valid.')
return result
def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None:
"""Pre-assign an item to a location"""
if item_name not in precollected:
self.exclude.append(item_name)
data = item_table[item_name]
item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player)
self.multiworld.get_location(location_name, self.player).place_locked_item(item)
def get_filler_item_name(self):
"""Getting a random ingredient item as filler"""
ingredients = []
for name, data in item_table.items():
if data.group == ItemGroup.INGREDIENT:
ingredients.append(name)
filler_item_name = self.random.choice(ingredients)
return filler_item_name
def create_items(self) -> None:
"""Create every item in the world"""
precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
if self.options.turtle_randomizer.value > 0:
if self.options.turtle_randomizer.value == 2:
self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected)
else:
self.__pre_fill_item("Transturtle Veil top left", "The veil top left area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Veil top right", "The veil top right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Open Water top right", "Open water top right area, Transturtle",
precollected)
self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
precollected)
self.__pre_fill_item("Transturtle Home water", "Home water, Transturtle", precollected)
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected)
# The last two are inverted because in the original game, they are special turtle that communicate directly
self.__pre_fill_item("Transturtle Simon says", "Arnassi Ruins, Transturtle", precollected)
self.__pre_fill_item("Transturtle Arnassi ruins", "Simon says area, Transturtle", precollected)
for name, data in item_table.items():
if name in precollected:
precollected.remove(name)
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
else:
if name not in self.exclude:
for i in range(data.count):
item = self.create_item(name)
self.multiworld.itempool.append(item)
def set_rules(self) -> None:
"""
Launched when the Multiworld generator is ready to generate rules
"""
self.regions.adjusting_rules(self.options)
self.multiworld.completion_condition[self.player] = lambda \
state: state.has("Victory", self.player)
def generate_basic(self) -> None:
"""
Player-specific randomization that does not affect logic.
Used to fill then `ingredients_substitution` list
"""
simple_ingredients_substitution = [i for i in range(27)]
if self.options.ingredient_randomizer.value > 0:
if self.options.ingredient_randomizer.value == 1:
simple_ingredients_substitution.pop(-1)
simple_ingredients_substitution.pop(-1)
simple_ingredients_substitution.pop(-1)
self.random.shuffle(simple_ingredients_substitution)
if self.options.ingredient_randomizer.value == 1:
simple_ingredients_substitution.extend([24, 25, 26])
dishes_substitution = [i for i in range(27, 76)]
if self.options.dish_randomizer:
self.random.shuffle(dishes_substitution)
self.ingredients_substitution.clear()
self.ingredients_substitution.extend(simple_ingredients_substitution)
self.ingredients_substitution.extend(dishes_substitution)
def fill_slot_data(self) -> Dict[str, Any]:
return {"ingredientReplacement": self.ingredients_substitution,
"aquarianTranslate": bool(self.options.aquarian_translation.value),
"secret_needed": self.options.objective.value > 0,
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
"skip_first_vision": bool(self.options.skip_first_vision.value),
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
}

View File

@@ -0,0 +1,64 @@
# Aquaria
## Game page in other languages:
* [Français](/games/Aquaria/info/fr)
## Where is the options page?
The player options page for this game contains all the options you need to configure and export a config file. Player
options page link: [Aquaria Player Options Page](../player-options).
## What does randomization do to this game?
The locations in the randomizer are:
- All sing bulbs;
- All Mithalas Urns;
- All Sunken City crates;
- Collectible treasure locations (including pet eggs and costumes);
- Beating Simon says;
- Li cave;
- Every Transportation Turtle (also called transturtle);
- Locations where you get songs,
* Erulian spirit cristal,
* Energy status mini-boss,
* Beating Mithalan God boss,
* Fish cave puzzle,
* Beating Drunian God boss,
* Beating Sun God boss,
* Breaking Li cage in the body
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered received.
The items in the randomizer are:
- Dishes (used to learn recipes*);
- Some ingredients;
- The Wok (third plate used to cook 3 ingredients recipes everywhere);
- All collectible treasure (including pet eggs and costumes);
- Li and Li song;
- All songs (other than Li's song since it is learned when Li is obtained);
- Transportation to transturtles.
Also, there is the option to randomize every ingredient drops (from fishes, monsters
or plants).
*Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
cannot be cooked (and learn) before being obtained as randomized items. Also, enemies and plants
that drop dishes that have not been learned before will drop ingredients of this dish instead.
## What is the goal of the game?
The goal of the Aquaria game is to beat the creator. You can also add other goals like getting
secret memories, beating a number of mini-bosses and beating a number of bosses.
## Which items can be in another player's world?
Any items specified above can be in another player's world.
## What does another world's item look like in Aquaria?
No visuals are shown when finding locations other than collectible treasure.
For those treasures, the visual of the treasure is visually unchanged.
After collecting a location check, a message will be shown to inform the player
what has been collected, and who will receive it.
## When the player receives an item, what happens?
When you receive an item, a message will pop up to inform you where you received
the item from, and which one it is.

View File

@@ -0,0 +1,65 @@
# Aquaria
## Où se trouve la page des options ?
La [page des options du joueur pour ce jeu](../player-options) contient tous
les options dont vous avez besoin pour configurer et exporter le fichier.
## Quel est l'effet de la randomisation sur ce jeu ?
Les localisations du "Ransomizer" sont:
- tous les bulbes musicaux;
- toutes les urnes de Mithalas;
- toutes les caisses de la cité engloutie;
- les localisations des trésors de collections (incluant les oeufs d'animaux de compagnie et les costumes);
- Battre Simom dit;
- La caverne de Li;
- Les tortues de transportation (transturtle);
- Localisation ou on obtient normalement les musiques,
* cristal de l'esprit Erulien,
* le mini-boss de la statue de l'énergie,
* battre le dieu de Mithalas,
* résoudre l'énigme de la caverne des poissons,
* battre le dieu Drunien,
* battre le dieu du soleil,
* détruire la cage de Li dans le corps,
À noter que, contrairement au jeu original, lors de l'ouverture d'un bulbe musical, d'une urne de Mithalas ou
d'une caisse de la cité engloutie, aucun objet n'en sortira. La localisation représentée par l'objet ouvert est reçue
dès l'ouverture.
Les objets pouvant être obtenus sont:
- les recettes (permettant d'apprendre les recettes*);
- certains ingrédients;
- le Wok (la troisième assiette permettant de cuisiner avec trois ingrédients n'importe où);
- Tous les trésors de collection (incluant les oeufs d'animal de compagnie et les costumes);
- Li et la musique de Li;
- Toutes les musiques (autre que la musique de Li puisque cette dernière est apprise en obtenant Li);
- Les localisations de transportation.
Il y a également l'option pour mélanger les ingrédients obtenus en éliminant des monstres, des poissons ou des plantes.
*À noter que, contrairement au jeu original, il est impossible de cuisiner une recette qui n'a pas préalablement
été apprise en obtenant un repas en tant qu'objet. À noter également que les ennemies et plantes qui
donnent un repas dont la recette n'a pas préalablement été apprise vont donner les ingrédients de cette
recette.
## Quel est le but de Aquaria ?
Dans Aquaria, le but est de battre le monstre final (le créateur). Il est également possible d'ajouter
des buts comme obtenir les trois souvenirs secrets, ou devoir battre une quantité de boss ou de mini-boss.
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
Tous les objets indiqués plus haut peuvent être obtenus à partir du monde d'un autre joueur.
## À quoi ressemble un objet d'un autre monde dans ce jeu
Autre que pour les trésors de collection (dont le visuel demeure inchangé),
les autres localisations n'ont aucun visuel. Lorsqu'une localisation randomisée est obtenue,
un message est affiché à l'écran pour indiquer quel objet a été trouvé et pour quel joueur.
## Que se passe-t-il lorsque le joueur reçoit un objet ?
Chaque fois qu'un objet est reçu, un message apparaît à l'écran pour en informer le joueur.

View File

@@ -0,0 +1,114 @@
# Aquaria Randomizer Setup Guide
## Required Software
- The original Aquaria Game (buyable from a lot of online game seller);
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
## Installation and execution Procedures
### Windows
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
Also, in Windows, the save files are stored in the Aquaria folder. So copying the Aquaria folder for every Multiworld
game you play will make sure that every game has their own save game.
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files
are those:
- aquaria_randomizer.exe
- OpenAL32.dll
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
- cacert.pem
If there is a conflict between file in the original game folder and the unzipped files, you should override
the original files with the one of the unzipped randomizer.
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
by writing `cmd` in the address bar of the Windows file explorer). Here is the command line to use to start the
randomizer:
```bash
aquaria_randomizer.exe --name YourName --server theServer:thePort
```
or, if the room has a password:
```bash
aquaria_randomizer.exe --name YourName --server theServer:thePort --password thePassword
```
### Linux when using the AppImage
If you use the AppImage, just copy it in the Aquaria game folder. You then have to make it executable. You
can do that from command line by using
```bash
chmod +x Aquaria_Randomizer-*.AppImage
```
or by using the Graphical Explorer of your system.
To launch the randomizer, just launch in command line:
```bash
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
```
or, if the room has a password:
```bash
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort --password thePassword
```
Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurred,
the preceding commands will launch the game multiple times.
### Linux when using the tar file
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted
files are those:
- aquaria_randomizer
- override (directory)
- usersettings.xml
- cacert.pem
If there is a conflict between file in the original game folder and the extracted files, you should override
the original files with the one of the extracted randomizer files.
Then, you should use your system package manager to install liblua5, libogg, libvorbis, libopenal and libsdl2.
On Debian base system (like Ubuntu), you can use the following command:
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
```
Also, if there is some `.so` files in the Aquaria original game folder (`libgcc_s.so.1`, `libopenal.so.1`,
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
old libraries that will not work on the recent build of the randomizer.
To launch the randomizer, just launch in command line:
```bash
./aquaria_randomizer --name YourName --server theServer:thePort
```
or, if the room has a password:
```bash
./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
```
Note: If you have a permission denied error when using the command line, you can use this command line to be
sure that your executable has executable permission:
```bash
chmod +x aquaria_randomizer
```

View File

@@ -0,0 +1,118 @@
# Guide de configuration MultiWorld d'Aquaria
## Logiciels nécessaires
- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne)
- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/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)
## Procédures d'installation et d'exécution
### Windows
En premier lieu, vous devriez effectuer une nouvelle copie du jeu d'Aquaria original à chaque fois que vous effectuez une
nouvelle partie. La première raison de cette copie est que le randomizer modifie des fichiers qui rendront possiblement
le jeu original non fonctionnel. La seconde raison d'effectuer cette copie est que les sauvegardes sont créées
directement dans le répertoire du jeu. Donc, la copie permet d'éviter de perdre vos sauvegardes du jeu d'origine ou
encore de charger une sauvegarde d'une ancienne partie de multiworld (ce qui pourrait avoir comme conséquence de briser
la logique du multiworld).
Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive dans le répertoire du jeu d'Aquaria. Le
fichier d'archive devrait contenir les fichiers suivants:
- aquaria_randomizer.exe
- OpenAL32.dll
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
- cacert.pem
S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
les fichiers contenus dans l'archive zip.
Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
la ligne de commande à utiliser pour lancer le randomizer:
```bash
aquaria_randomizer.exe --name VotreNom --server leServeur:LePort
```
ou, si vous devez entrer un mot de passe:
```bash
aquaria_randomizer.exe --name VotreNom --server leServeur:LePort --password leMotDePasse
```
### Linux avec le fichier AppImage
Si vous utilisez le fichier AppImage, copiez le fichier dans le répertoire du jeu d'Aquaria. Ensuite, assurez-vous de
le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la commande suivante:
```bash
chmod +x Aquaria_Randomizer-*.AppImage
```
ou bien en utilisant l'explorateur graphique de votre système.
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
```
Si vous devez entrer un mot de passe:
```bash
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort --password LeMotDePasse
```
À noter que vous ne devez pas avoir plusieurs fichiers AppImage différents dans le même répertoire. Si cette situation
survient, le jeu sera lancé plusieurs fois.
### Linux avec le fichier tar
En premier lieu, assurez-vous de faire une copie du répertoire du jeu d'origine d'Aquaria. Les fichiers contenus
dans le randomizer auront comme impact de rendre le jeu d'origine non fonctionnel. Donc, effectuer la copie du jeu
avant de déposer le randomizer à l'intérieur permet de vous assurer de garder une version du jeu d'origine fonctionnel.
Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
fichiers extraient du fichier tar devraient être les suivants:
- aquaria_randomizer
- override (directory)
- usersettings.xml
- cacert.pem
S'il y a des conflits entre les fichiers de l'archive tar et les fichiers du jeu original, vous devez utiliser
les fichiers contenus dans l'archive tar.
Ensuite, vous devez installer manuellement les librairies dont dépend le jeu: liblua5, libogg, libvorbis, libopenal and
libsdl2. Vous pouvez utiliser le système de "package" de votre système pour les installer. Voici un exemple avec
Debian (et Ubuntu):
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
```
Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (`libgcc_s.so.1`, `libopenal.so.1`,
`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./aquaria_randomizer --name VotreNom --server LeServeur:LePort
```
Si vous devez entrer un mot de passe:
```bash
./aquaria_randomizer --name VotreNom --server LeServeur:LePort --password LeMotDePasse
```
Note: Si vous avez une erreur de permission lors de l'exécution du randomizer, vous pouvez utiliser cette commande
pour vous assurer que votre fichier est exécutable:
```bash
chmod +x aquaria_randomizer
```

View File

@@ -0,0 +1,218 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Base class for the Aquaria randomizer unit tests
"""
from test.bases import WorldTestBase
# Every location accessible after the home water.
after_home_water_locations = [
"Sun Crystal",
"Home water, Transturtle",
"Open water top left area, bulb under the rock in the right path",
"Open water top left area, bulb under the rock in the left path",
"Open water top left area, bulb to the right of the save cristal",
"Open water top right area, bulb in the small path before Mithalas",
"Open water top right area, bulb in the path from the left entrance",
"Open water top right area, bulb in the clearing close to the bottom exit",
"Open water top right area, bulb in the big clearing close to the save cristal",
"Open water top right area, bulb in the big clearing to the top exit",
"Open water top right area, first urn in the Mithalas exit",
"Open water top right area, second urn in the Mithalas exit",
"Open water top right area, third urn in the Mithalas exit",
"Open water top right area, bulb in the turtle room",
"Open water top right area, Transturtle",
"Open water bottom left area, bulb behind the chomper fish",
"Open water bottom left area, bulb inside the downest fish pass",
"Open water skeleton path, bulb close to the right exit",
"Open water skeleton path, bulb behind the chomper fish",
"Open water skeleton path, King skull",
"Arnassi Ruins, bulb in the right part",
"Arnassi Ruins, bulb in the left part",
"Arnassi Ruins, bulb in the center part",
"Arnassi ruins, Song plant spore on the top of the ruins",
"Arnassi ruins, Arnassi Armor",
"Arnassi Ruins, Arnassi statue",
"Arnassi Ruins, Transturtle",
"Arnassi ruins, Crab armor",
"Simon says area, Transturtle",
"Mithalas city, first bulb in the left city part",
"Mithalas city, second bulb in the left city part",
"Mithalas city, bulb in the right part",
"Mithalas city, bulb at the top of the city",
"Mithalas city, first bulb in a broken home",
"Mithalas city, second bulb in a broken home",
"Mithalas city, bulb in the bottom left part",
"Mithalas city, first bulb in one of the homes",
"Mithalas city, second bulb in one of the homes",
"Mithalas city, first urn in one of the homes",
"Mithalas city, second urn in one of the homes",
"Mithalas city, first urn in the city reserve",
"Mithalas city, second urn in the city reserve",
"Mithalas city, third urn in the city reserve",
"Mithalas city, first bulb at the end of the top path",
"Mithalas city, second bulb at the end of the top path",
"Mithalas city, bulb in the top path",
"Mithalas city, Mithalas pot",
"Mithalas city, urn in the cathedral flower tube entrance",
"Mithalas city, Doll",
"Mithalas city, urn inside a home fish pass",
"Mithalas city castle, bulb in the flesh hole",
"Mithalas city castle, Blue banner",
"Mithalas city castle, urn in the bedroom",
"Mithalas city castle, first urn of the single lamp path",
"Mithalas city castle, second urn of the single lamp path",
"Mithalas city castle, urn in the bottom room",
"Mithalas city castle, first urn on the entrance path",
"Mithalas city castle, second urn on the entrance path",
"Mithalas castle, beating the priests",
"Mithalas city castle, Trident head",
"Mithalas cathedral, first urn in the top right room",
"Mithalas cathedral, second urn in the top right room",
"Mithalas cathedral, third urn in the top right room",
"Mithalas cathedral, urn in the flesh room with fleas",
"Mithalas cathedral, first urn in the bottom right path",
"Mithalas cathedral, second urn in the bottom right path",
"Mithalas cathedral, urn behind the flesh vein",
"Mithalas cathedral, urn in the top left eyes boss room",
"Mithalas cathedral, first urn in the path behind the flesh vein",
"Mithalas cathedral, second urn in the path behind the flesh vein",
"Mithalas cathedral, third urn in the path behind the flesh vein",
"Mithalas cathedral, one of the urns in the top right room",
"Mithalas cathedral, Mithalan Dress",
"Mithalas cathedral right area, urn bellow the left entrance",
"Cathedral underground, bulb in the center part",
"Cathedral underground, first bulb in the top left part",
"Cathedral underground, second bulb in the top left part",
"Cathedral underground, third bulb in the top left part",
"Cathedral underground, bulb close to the save cristal",
"Cathedral underground, bulb in the bottom right path",
"Cathedral boss area, beating Mithalan God",
"Kelp Forest top left area, bulb in the bottom left clearing",
"Kelp Forest top left area, bulb in the path down from the top left clearing",
"Kelp Forest top left area, bulb in the top left clearing",
"Kelp Forest top left, Jelly Egg",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Kelp Forest top right area, bulb under the rock in the right path",
"Kelp Forest top right area, bulb at the left of the center clearing",
"Kelp Forest top right area, bulb in the left path's big room",
"Kelp Forest top right area, bulb in the left path's small room",
"Kelp Forest top right area, bulb at the top of the center clearing",
"Kelp forest top right area, Black pearl",
"Kelp Forest top right area, bulb in the top fish pass",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp forest bottom left area, Walker baby",
"Kelp Forest bottom left area, Transturtle",
"Kelp forest bottom right area, Odd Container",
"Kelp forest boss area, beating Drunian God",
"Kelp Forest boss room, bulb at the bottom of the area",
"Kelp Forest bottom left area, Fish cave puzzle",
"Kelp Forest sprite cave, bulb inside the fish pass",
"Kelp Forest sprite cave, bulb in the second room",
"Kelp Forest Sprite Cave, Seed bag",
"Mermog cave, bulb in the left part of the cave",
"Mermog cave, Piranha Egg",
"The veil top left area, In the Li cave",
"The veil top left area, bulb under the rock in the top right path",
"The veil top left area, bulb hidden behind the blocking rock",
"The veil top left area, Transturtle",
"The veil top left area, bulb inside the fish pass",
"Turtle cave, Turtle Egg",
"Turtle cave, bulb in bubble cliff",
"Turtle cave, Urchin costume",
"The veil top right area, bulb in the middle of the wall jump cliff",
"The veil top right area, golden starfish at the bottom right of the bottom path",
"The veil top right area, bulb in the top of the water fall",
"The veil top right area, Transturtle",
"The veil bottom area, bulb in the left path",
"The veil bottom area, bulb in the spirit path",
"The veil bottom area, Verse egg",
"The veil bottom area, Stone Head",
"Octopus cave, Dumbo Egg",
"Octopus cave, bulb in the path below the octopus cave path",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Sun temple, bulb in the top left part",
"Sun temple, bulb in the top right part",
"Sun temple, bulb at the top of the high dark room",
"Sun temple, Golden Gear",
"Sun temple, first bulb of the temple",
"Sun temple, bulb on the left part",
"Sun temple, bulb in the hidden room of the right part",
"Sun temple, Sun key",
"Sun Worm path, first path bulb",
"Sun Worm path, second path bulb",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"Sun temple boss area, beating Sun God",
"Abyss left area, bulb in hidden path room",
"Abyss left area, bulb in the right part",
"Abyss left area, Glowing seed",
"Abyss left area, Glowing Plant",
"Abyss left area, bulb in the bottom fish pass",
"Abyss right area, bulb behind the rock in the whale room",
"Abyss right area, bulb in the middle path",
"Abyss right area, bulb behind the rock in the middle path",
"Abyss right area, bulb in the left green room",
"Abyss right area, Transturtle",
"Ice cave, bulb in the room to the right",
"Ice cave, First bulbs in the top exit room",
"Ice cave, Second bulbs in the top exit room",
"Ice cave, third bulbs in the top exit room",
"Ice cave, bulb in the left room",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"The whale, Verse egg",
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"The body center area, breaking li cage",
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
"The body left area, bulb bellow the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
"The body right area, bulb in the top path to the bottom face room",
"The body right area, bulb in the bottom face room",
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, first bulb in the turtle room",
"Final boss area, second bulbs in the turtle room",
"Final boss area, third bulbs in the turtle room",
"Final boss area, Transturtle",
"Final boss area, bulb in the boss third form room",
"Kelp forest, beating Simon says",
"Beating Fallen God",
"Beating Mithalan God",
"Beating Drunian God",
"Beating Sun God",
"Beating the Golem",
"Beating Nautilus Prime",
"Beating Blaster Peg Prime",
"Beating Mergog",
"Beating Mithalan priests",
"Beating Octopus Prime",
"Beating Crabbius Maximus",
"Beating Mantis Shrimp Prime",
"Beating King Jellyfish God Prime",
"First secret",
"Second secret",
"Third secret",
"Sunken City cleared",
"Objective complete",
]
class AquariaTestBase(WorldTestBase):
"""Base class for Aquaria unit tests"""
game = "Aquaria"

View File

@@ -0,0 +1,48 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the beast form
"""
from worlds.aquaria.test import AquariaTestBase
class BeastFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the beast form"""
def test_beast_form_location(self) -> None:
"""Test locations that require beast form"""
locations = [
"Mithalas castle, beating the priests",
"Arnassi ruins, Crab armor",
"Arnassi ruins, Song plant spore on the top of the ruins",
"Mithalas city, first bulb at the end of the top path",
"Mithalas city, second bulb at the end of the top path",
"Mithalas city, bulb in the top path",
"Mithalas city, Mithalas pot",
"Mithalas city, urn in the cathedral flower tube entrance",
"Mermog cave, Piranha Egg",
"Mithalas cathedral, Mithalan Dress",
"Turtle cave, bulb in bubble cliff",
"Turtle cave, Urchin costume",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The veil top right area, bulb in the top of the water fall",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Octopus cave, Dumbo Egg",
"Beating the Golem",
"Beating Mergog",
"Beating Crabbius Maximus",
"Beating Octopus Prime",
"Beating Mantis Shrimp Prime",
"King Jellyfish cave, Jellyfish Costume",
"King Jellyfish cave, bulb in the right path from King Jelly",
"Beating King Jellyfish God Prime",
"Beating Mithalan priests",
"Sunken City cleared"
]
items = [["Beast form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,36 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (without the location
under rock needing bind song option)
"""
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
class BindSongAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the bind song"""
options = {
"bind_song_needed_to_get_under_rock_bulb": False,
}
def test_bind_song_location(self) -> None:
"""Test locations that require Bind song"""
locations = [
"Verse cave right area, Big Seed",
"Home water, bulb in the path bellow Nautilus Prime",
"Home water, bulb in the bottom left room",
"Home water, Nautilus Egg",
"Song cave, Verse egg",
"Energy temple first area, beating the energy statue",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple first area, Energy Idol",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
*after_home_water_locations
]
items = [["Bind song"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,42 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (with the location
under rock needing bind song option)
"""
from worlds.aquaria.test import AquariaTestBase
from worlds.aquaria.test.test_bind_song_access import after_home_water_locations
class BindSongOptionAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the bind song"""
options = {
"bind_song_needed_to_get_under_rock_bulb": True,
}
def test_bind_song_location(self) -> None:
"""Test locations that require Bind song with the bind song needed option activated"""
locations = [
"Verse cave right area, Big Seed",
"Verse cave left area, bulb under the rock at the end of the path",
"Home water, bulb under the rock in the left path from the verse cave",
"Song cave, bulb under the rock close to the song door",
"Song cave, bulb under the rock in the path to the singing statues",
"Naija's home, bulb under the rock at the right of the main path",
"Home water, bulb in the path bellow Nautilus Prime",
"Home water, bulb in the bottom left room",
"Home water, Nautilus Egg",
"Song cave, Verse egg",
"Energy temple first area, beating the energy statue",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple first area, Energy Idol",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
*after_home_water_locations
]
items = [["Bind song"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,20 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the home water confine via option
"""
from worlds.aquaria.test import AquariaTestBase
class ConfinedHomeWaterAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option disabled"""
options = {
"unconfine_home_water": 0,
"early_energy_form": False
}
def test_confine_home_water_location(self) -> None:
"""Test region accessible with confined home water"""
self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area")
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")

View File

@@ -0,0 +1,26 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the dual song
"""
from worlds.aquaria.test import AquariaTestBase
class LiAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the dual song"""
options = {
"turtle_randomizer": 1,
}
def test_li_song_location(self) -> None:
"""Test locations that require the dual song"""
locations = [
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, bulb in the boss third form room",
"Objective complete"
]
items = [["Dual form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,73 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (without the early
energy form option)
"""
from worlds.aquaria.test import AquariaTestBase
class EnergyFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the energy form"""
options = {
"early_energy_form": False,
}
def test_energy_form_location(self) -> None:
"""Test locations that require Energy form"""
locations = [
"Home water, Nautilus Egg",
"Naija's home, bulb after the energy door",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
"Mithalas castle, beating the priests",
"Mithalas cathedral, first urn in the top right room",
"Mithalas cathedral, second urn in the top right room",
"Mithalas cathedral, third urn in the top right room",
"Mithalas cathedral, urn in the flesh room with fleas",
"Mithalas cathedral, first urn in the bottom right path",
"Mithalas cathedral, second urn in the bottom right path",
"Mithalas cathedral, urn behind the flesh vein",
"Mithalas cathedral, urn in the top left eyes boss room",
"Mithalas cathedral, first urn in the path behind the flesh vein",
"Mithalas cathedral, second urn in the path behind the flesh vein",
"Mithalas cathedral, third urn in the path behind the flesh vein",
"Mithalas cathedral, one of the urns in the top right room",
"Mithalas cathedral, Mithalan Dress",
"Mithalas cathedral right area, urn bellow the left entrance",
"Cathedral boss area, beating Mithalan God",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Kelp forest boss area, beating Drunian God",
"Mermog cave, Piranha Egg",
"Octopus cave, Dumbo Egg",
"Sun temple boss area, beating Sun God",
"Arnassi ruins, Crab armor",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Final boss area, bulb in the boss third form room",
"Beating Fallen God",
"Beating Mithalan God",
"Beating Drunian God",
"Beating Sun God",
"Beating the Golem",
"Beating Nautilus Prime",
"Beating Blaster Peg Prime",
"Beating Mergog",
"Beating Mithalan priests",
"Beating Octopus Prime",
"Beating Crabbius Maximus",
"Beating King Jellyfish God Prime",
"First secret",
"Sunken City cleared",
"Objective complete",
]
items = [["Energy form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,31 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (with the early
energy form option)
"""
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
class EnergyFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the energy form"""
options = {
"early_energy_form": True,
}
def test_energy_form_location(self) -> None:
"""Test locations that require Energy form with early energy song enable"""
locations = [
"Home water, Nautilus Egg",
"Naija's home, bulb after the energy door",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
*after_home_water_locations
]
items = [["Energy form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,37 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the fish form
"""
from worlds.aquaria.test import AquariaTestBase
class FishFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the fish form"""
options = {
"turtle_randomizer": 1,
}
def test_fish_form_location(self) -> None:
"""Test locations that require fish form"""
locations = [
"The veil top left area, bulb inside the fish pass",
"Mithalas city, Doll",
"Mithalas city, urn inside a home fish pass",
"Kelp Forest top right area, bulb in the top fish pass",
"The veil bottom area, Verse egg",
"Open water bottom left area, bulb inside the downest fish pass",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Mermog cave, bulb in the left part of the cave",
"Mermog cave, Piranha Egg",
"Beating Mergog",
"Octopus cave, Dumbo Egg",
"Octopus cave, bulb in the path below the octopus cave path",
"Beating Octopus Prime",
"Abyss left area, bulb in the bottom fish pass",
"Arnassi ruins, Arnassi Armor"
]
items = [["Fish form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,45 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without Li
"""
from worlds.aquaria.test import AquariaTestBase
class LiAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without Li"""
options = {
"turtle_randomizer": 1,
}
def test_li_song_location(self) -> None:
"""Test locations that require Li"""
locations = [
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"The body center area, breaking li cage",
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
"The body left area, bulb bellow the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
"The body right area, bulb in the top path to the bottom face room",
"The body right area, bulb in the bottom face room",
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, bulb in the boss third form room",
"Beating the Golem",
"Sunken City cleared",
"Objective complete"
]
items = [["Li and Li song", "Body tongue cleared"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,71 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
"""
from worlds.aquaria.test import AquariaTestBase
class LightAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without light"""
options = {
"turtle_randomizer": 1,
"light_needed_to_get_to_dark_places": True,
}
def test_light_location(self) -> None:
"""Test locations that require light"""
locations = [
# Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
# tested.
# "Third secret",
# "Sun temple, bulb in the top left part",
# "Sun temple, bulb in the top right part",
# "Sun temple, bulb at the top of the high dark room",
# "Sun temple, Golden Gear",
# "Sun Worm path, first path bulb",
# "Sun Worm path, second path bulb",
# "Sun Worm path, first cliff bulb",
"Octopus cave, Dumbo Egg",
"Kelp forest bottom right area, Odd Container",
"Kelp forest top right area, Black pearl",
"Abyss left area, bulb in hidden path room",
"Abyss left area, bulb in the right part",
"Abyss left area, Glowing seed",
"Abyss left area, Glowing Plant",
"Abyss left area, bulb in the bottom fish pass",
"Abyss right area, bulb behind the rock in the whale room",
"Abyss right area, bulb in the middle path",
"Abyss right area, bulb behind the rock in the middle path",
"Abyss right area, bulb in the left green room",
"Abyss right area, Transturtle",
"Ice cave, bulb in the room to the right",
"Ice cave, First bulbs in the top exit room",
"Ice cave, Second bulbs in the top exit room",
"Ice cave, third bulbs in the top exit room",
"Ice cave, bulb in the left room",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Beating Mantis Shrimp Prime",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Beating King Jellyfish God Prime",
"The whale, Verse egg",
"First secret",
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Sunken City cleared",
"Beating the Golem",
"Beating Octopus Prime",
"Final boss area, bulb in the boss third form room",
"Objective complete",
]
items = [["Sun form", "Baby dumbo", "Has sun crystal"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,57 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the nature form
"""
from worlds.aquaria.test import AquariaTestBase
class NatureFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the nature form"""
options = {
"turtle_randomizer": 1,
}
def test_nature_form_location(self) -> None:
"""Test locations that require nature form"""
locations = [
"Song cave, Anemone seed",
"Energy temple blaster room, Blaster egg",
"Beating Blaster Peg Prime",
"Kelp forest top left area, Verse egg",
"Kelp Forest top left area, bulb close to the Verse egg",
"Mithalas castle, beating the priests",
"Kelp Forest sprite cave, bulb in the second room",
"Kelp Forest Sprite Cave, Seed bag",
"Beating Mithalan priests",
"Abyss left area, bulb in the bottom fish pass",
"Bubble cave, Verse egg",
"Beating Mantis Shrimp Prime",
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Beating the Golem",
"Sunken City cleared",
"The body center area, breaking li cage",
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
"The body left area, bulb bellow the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
"The body right area, bulb in the top path to the bottom face room",
"The body right area, bulb in the bottom face room",
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, bulb in the boss third form room",
"Objective complete"
]
items = [["Nature form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,60 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
"""
from worlds.aquaria.test import AquariaTestBase
from BaseClasses import ItemClassification
class UNoProgressionHardHiddenTest(AquariaTestBase):
"""Unit test used to test that no progression items can be put in hard or hidden locations when option enabled"""
options = {
"no_progression_hard_or_hidden_locations": True
}
unfillable_locations = [
"Energy temple boss area, Fallen god tooth",
"Cathedral boss area, beating Mithalan God",
"Kelp forest boss area, beating Drunian God",
"Sun temple boss area, beating Sun God",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Home water, Nautilus Egg",
"Energy temple blaster room, Blaster egg",
"Mithalas castle, beating the priests",
"Mermog cave, Piranha Egg",
"Octopus cave, Dumbo Egg",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Final boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The veil top right area, bulb in the top of the water fall",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp forest bottom left area, Walker baby",
"Sun temple, Sun key",
"The body bottom area, Mutant Costume",
"Sun temple, bulb in the hidden room of the right part",
"Arnassi ruins, Arnassi Armor",
]
def test_unconfine_home_water_both_location_fillable(self) -> None:
"""
Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
"""
for location in self.unfillable_locations:
for item_name in self.world.item_names:
item = self.get_item_by_name(item_name)
if item.classification == ItemClassification.progression:
self.assertFalse(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
else:
self.assertTrue(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")

View File

@@ -0,0 +1,53 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
"""
from worlds.aquaria.test import AquariaTestBase
from BaseClasses import ItemClassification
class UNoProgressionHardHiddenTest(AquariaTestBase):
"""Unit test used to test that no progression items can be put in hard or hidden locations when option disabled"""
options = {
"no_progression_hard_or_hidden_locations": False
}
unfillable_locations = [
"Energy temple boss area, Fallen god tooth",
"Cathedral boss area, beating Mithalan God",
"Kelp forest boss area, beating Drunian God",
"Sun temple boss area, beating Sun God",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Home water, Nautilus Egg",
"Energy temple blaster room, Blaster egg",
"Mithalas castle, beating the priests",
"Mermog cave, Piranha Egg",
"Octopus cave, Dumbo Egg",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Final boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The veil top right area, bulb in the top of the water fall",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp forest bottom left area, Walker baby",
"Sun temple, Sun key",
"The body bottom area, Mutant Costume",
"Sun temple, bulb in the hidden room of the right part",
"Arnassi ruins, Arnassi Armor",
]
def test_unconfine_home_water_both_location_fillable(self) -> None:
"""Unit test used to test that progression items can be put in hard or hidden locations when option disabled"""
for location in self.unfillable_locations:
for item_name in self.world.item_names:
item = self.get_item_by_name(item_name)
self.assertTrue(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")

View File

@@ -0,0 +1,36 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the spirit form
"""
from worlds.aquaria.test import AquariaTestBase
class SpiritFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the spirit form"""
def test_spirit_form_location(self) -> None:
"""Test locations that require spirit form"""
locations = [
"The veil bottom area, bulb in the spirit path",
"Mithalas city castle, Trident head",
"Open water skeleton path, King skull",
"Kelp forest bottom left area, Walker baby",
"Abyss right area, bulb behind the rock in the whale room",
"The whale, Verse egg",
"Ice cave, bulb in the room to the right",
"Ice cave, First bulbs in the top exit room",
"Ice cave, Second bulbs in the top exit room",
"Ice cave, third bulbs in the top exit room",
"Ice cave, bulb in the left room",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Sunken city left area, Girl Costume",
"Beating Mantis Shrimp Prime",
"First secret",
"Arnassi ruins, Arnassi Armor",
]
items = [["Spirit form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,25 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the sun form
"""
from worlds.aquaria.test import AquariaTestBase
class SunFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the sun form"""
def test_sun_form_location(self) -> None:
"""Test locations that require sun form"""
locations = [
"First secret",
"The whale, Verse egg",
"Abyss right area, bulb behind the rock in the whale room",
"Octopus cave, Dumbo Egg",
"Beating Octopus Prime",
"Final boss area, bulb in the boss third form room",
"Objective complete"
]
items = [["Sun form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,21 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the unconfined home water option via transportation
turtle and energy door
"""
from worlds.aquaria.test import AquariaTestBase
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
options = {
"unconfine_home_water": 3,
"early_energy_form": False
}
def test_unconfine_home_water_both_location(self) -> None:
"""Test locations accessible with unconfined home water via energy door and transportation turtle"""
self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area")
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")

View File

@@ -0,0 +1,20 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
"""
from worlds.aquaria.test import AquariaTestBase
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
options = {
"unconfine_home_water": 1,
"early_energy_form": False
}
def test_unconfine_home_water_energy_door_location(self) -> None:
"""Test locations accessible with unconfined home water via energy door"""
self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area")
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")

View File

@@ -0,0 +1,20 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
"""
from worlds.aquaria.test import AquariaTestBase
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
options = {
"unconfine_home_water": 2,
"early_energy_form": False
}
def test_unconfine_home_water_transturtle_location(self) -> None:
"""Test locations accessible with unconfined home water via transportation turtle"""
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area")

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